Mybatis源碼分析——Select語(yǔ)句的執(zhí)行過(guò)程分析(上)

前言

上一篇我們分析了Mapper接口代理類的生成珠十,本篇接著分析是如何調(diào)用到XML中的SQL

我們回顧一下MapperMethod 的execute方法

public class MapperMethod {
    
    //包含SQL相關(guān)信息医舆,比喻MappedStatement的id屬性瞻讽,(mapper.UserMapper.getAll)
    private final SqlCommand command;

    //包含了關(guān)于執(zhí)行的Mapper方法的參數(shù)類型和返回類型。
    private final MethodSignature method;

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        
        // 根據(jù) SQL 類型執(zhí)行相應(yīng)的數(shù)據(jù)庫(kù)操作
        switch (command.getType()) {
            case INSERT: {
                // 對(duì)用戶傳入的參數(shù)進(jìn)行轉(zhuǎn)換菩鲜,下同
                Object param = method.convertArgsToSqlCommandParam(args);
                // 執(zhí)行插入操作勾效,rowCountResult 方法用于處理返回值
                result = rowCountResult(sqlSession.insert(command.getName(), param));
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                 // 執(zhí)行更新操作
                result = rowCountResult(sqlSession.update(command.getName(), param));
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                // 執(zhí)行刪除操作
                result = rowCountResult(sqlSession.delete(command.getName(), param));
                break;
            }
            case SELECT:
                // 根據(jù)目標(biāo)方法的返回類型進(jìn)行相應(yīng)的查詢操作
                if (method.returnsVoid() && method.hasResultHandler()) {
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {
                    // 執(zhí)行查詢操作各吨,并返回多個(gè)結(jié)果 
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) {
                    // 執(zhí)行查詢操作,并將結(jié)果封裝在 Map 中返回
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) {
                    // 執(zhí)行查詢操作沦寂,并返回一個(gè) Cursor 對(duì)象
                    result = executeForCursor(sqlSession, args);
                } else {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    // 執(zhí)行查詢操作学密,并返回一個(gè)結(jié)果
                    result = sqlSession.selectOne(command.getName(), param);
                }
                break;
            case FLUSH:
                // 執(zhí)行刷新操作
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
            throw new BindingException("Mapper method '" + command.getName() 
            + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
    }
}

selectOne 方法分析

本節(jié)選擇分析 selectOne 方法,主要是因?yàn)?selectOne 在內(nèi)部會(huì)調(diào)用 selectList 方法传藏。同時(shí)分析 selectOne 方法等同于分析 selectList 方法腻暮。代碼如下

result = sqlSession.selectOne(command.getName(), param);

我們看到是通過(guò)sqlSession來(lái)執(zhí)行查詢的彤守,并且傳入的參數(shù)為command.getName()和param,也就是namespace.methodName(mapper.UserMapper.getAll)和方法的運(yùn)行參數(shù)哭靖。我們知道了具垫,所有的數(shù)據(jù)庫(kù)操作都是交給sqlSession來(lái)執(zhí)行的,那我們就來(lái)看看sqlSession的方法

public class DefaultSqlSession implements SqlSession {

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        // Popular vote was to return null on 0 results and throw exception on too many.
        // 調(diào)用 selectList 獲取結(jié)果
        List<T> list = this.<T>selectList(statement, parameter);
        if (list.size() == 1) {
            // 返回結(jié)果
            return list.get(0);
        } else if (list.size() > 1) {
            // 如果查詢結(jié)果大于1則拋出異常
            throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }
}

如上试幽,selectOne 方法在內(nèi)部調(diào)用 selectList 了方法筝蚕,并取 selectList 返回值的第1個(gè)元素作為自己的返回值。如果 selectList 返回的列表元素大于1铺坞,則拋出異常起宽。下面我們來(lái)看看 selectList 方法的實(shí)現(xiàn)。

DefaultSqlSession

public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;

    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
        // 調(diào)用重載方法
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }
    
    @Override
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            // 通過(guò)MappedStatement的Id獲取 MappedStatement
            MappedStatement ms = configuration.getMappedStatement(statement);
            // 調(diào)用 Executor 實(shí)現(xiàn)類中的 query 方法
            return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
}

我們之前創(chuàng)建DefaultSqlSession的時(shí)候济榨,是創(chuàng)建了一個(gè)Executor的實(shí)例作為其屬性的坯沪,我們看到通過(guò)MappedStatement的Id獲取 MappedStatement后,就交由Executor去執(zhí)行了

我們回顧一下前面的文章擒滑,Executor的創(chuàng)建過(guò)程腐晾,代碼如下

public class Configuration {

    protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;

    public Executor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
    }

    //創(chuàng)建一個(gè)執(zhí)行器,默認(rèn)是SIMPLE
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        //根據(jù)executorType來(lái)創(chuàng)建相應(yīng)的執(zhí)行器,Configuration默認(rèn)是SIMPLE
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            //創(chuàng)建SimpleExecutor實(shí)例丐一,并且包含Configuration和transaction屬性
            executor = new SimpleExecutor(this, transaction);
        }
        
        //如果要求緩存藻糖,生成另一種CachingExecutor,裝飾者模式,默認(rèn)都是返回CachingExecutor
        /**
         * 二級(jí)緩存開(kāi)關(guān)配置示例
         * <settings>
         *   <setting name="cacheEnabled" value="true"/>
         * </settings>
         */
        if (cacheEnabled) {
            //CachingExecutor使用裝飾器模式,將executor的功能添加上了二級(jí)緩存的功能钝诚,二級(jí)緩存會(huì)單獨(dú)文章來(lái)講
            executor = new CachingExecutor(executor);
        }
        
        //此處調(diào)用插件,通過(guò)插件可以改變Executor行為颖御,此處我們后面單獨(dú)文章講
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
}

executor包含了Configuration和Transaction,默認(rèn)的執(zhí)行器為SimpleExecutor凝颇,如果開(kāi)啟了二級(jí)緩存(默認(rèn)開(kāi)啟)潘拱,則CachingExecutor會(huì)包裝SimpleExecutor,那么我們?cè)摽碈achingExecutor的query方法了

public class CachingExecutor implements Executor {

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        // 獲取 BoundSql
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // 創(chuàng)建 CacheKey
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        // 調(diào)用重載方法
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

上面的代碼用于獲取 BoundSql 對(duì)象拧略,創(chuàng)建 CacheKey 對(duì)象芦岂,然后再將這兩個(gè)對(duì)象傳給重載方法。CacheKey 以及接下來(lái)即將出現(xiàn)的一二級(jí)緩存將會(huì)獨(dú)立成文進(jìn)行分析垫蛆。

獲取 BoundSql

// 獲取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);

調(diào)用了MappedStatement的getBoundSql方法禽最,并將運(yùn)行時(shí)參數(shù)傳入其中,我們大概的猜一下袱饭,這里是不是拼接SQL語(yǔ)句呢川无,并將運(yùn)行時(shí)參數(shù)設(shè)置到SQL語(yǔ)句中?

我們都知道 SQL 是配置在映射文件中的虑乖,但由于映射文件中的 SQL 可能會(huì)包含占位符 #{}懦趋,以及動(dòng)態(tài) SQL 標(biāo)簽,比如 <if>疹味、<where> 等仅叫。因此帜篇,我們并不能直接使用映射文件中配置的 SQL。MyBatis 會(huì)將映射文件中的 SQL 解析成一組 SQL 片段诫咱。我們需要對(duì)這一組片段進(jìn)行解析笙隙,從每個(gè)片段對(duì)象中獲取相應(yīng)的內(nèi)容。然后將這些內(nèi)容組合起來(lái)即可得到一個(gè)完成的 SQL 語(yǔ)句坎缭,這個(gè)完整的 SQL 以及其他的一些信息最終會(huì)存儲(chǔ)在 BoundSql 對(duì)象中竟痰。下面我們來(lái)看一下 BoundSql 類的成員變量信息,如下:

public class BoundSql {

    private final String sql;
    private final List<ParameterMapping> parameterMappings;
    private final Object parameterObject;
    private final Map<String, Object> additionalParameters;
    private final MetaObject metaParameters;
}

下面用一個(gè)表格列舉各個(gè)成員變量的含義幻锁。

變量名 類型 用途
sql String 一個(gè)完整的 SQL 語(yǔ)句凯亮,可能會(huì)包含問(wèn)號(hào) ? 占位符
parameterMappings List 參數(shù)映射列表,SQL 中的每個(gè) #{xxx} 占位符都會(huì)被解析成相應(yīng)的 ParameterMapping 對(duì)象
parameterObject Object 運(yùn)行時(shí)參數(shù)哄尔,即用戶傳入的參數(shù)宽堆,比如 Article 對(duì)象瓷马,或是其他的參數(shù)
additionalParameters Map 附加參數(shù)集合,用于存儲(chǔ)一些額外的信息,比如 datebaseId 等
metaParameters MetaObject additionalParameters 的元信息對(duì)象

接下來(lái)我們接著MappedStatement 的 getBoundSql 方法靠胜,代碼如下:

public final class MappedStatement {

    public BoundSql getBoundSql(Object parameterObject) {
    
        // 調(diào)用 sqlSource 的 getBoundSql 獲取 BoundSql凰棉,把method運(yùn)行時(shí)參數(shù)傳進(jìn)去
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings == null || parameterMappings.isEmpty()) {
            boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
        }

        // check for nested result maps in parameter mappings (issue #30)
        for (ParameterMapping pm : boundSql.getParameterMappings()) {
            String rmId = pm.getResultMapId();
            if (rmId != null) {
                ResultMap rm = configuration.getResultMap(rmId);
                if (rm != null) {
                    hasNestedResultMaps |= rm.hasNestedResultMaps();
                }
            }
        }

        return boundSql;
    }
}

MappedStatement 的 getBoundSql 在內(nèi)部調(diào)用了 SqlSource 實(shí)現(xiàn)類的 getBoundSql 方法档桃,并把method運(yùn)行時(shí)參數(shù)傳進(jìn)去昧廷,SqlSource 是一個(gè)接口,它有如下幾個(gè)實(shí)現(xiàn)類:

  • DynamicSqlSource
  • RawSqlSource
  • StaticSqlSource
  • ProviderSqlSource
  • VelocitySqlSource

當(dāng) SQL 配置中包含 ${}(不是 #{})占位符窄锅,或者包含 <if>创千、<where> 等標(biāo)簽時(shí),會(huì)被認(rèn)為是動(dòng)態(tài) SQL入偷,此時(shí)使用 DynamicSqlSource 存儲(chǔ) SQL 片段追驴。否則,使用 RawSqlSource 存儲(chǔ) SQL 配置信息疏之。我們來(lái)看看DynamicSqlSource的getBoundSql

DynamicSqlSource
public class DynamicSqlSource implements SqlSource {

    private final Configuration configuration;
    private final SqlNode rootSqlNode;

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        // 創(chuàng)建 DynamicContext
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        
        // 解析 SQL 片段殿雪,并將解析結(jié)果存儲(chǔ)到 DynamicContext 中,
        // 這里會(huì)將${}替換成method對(duì)應(yīng)的運(yùn)行時(shí)參數(shù)锋爪,也會(huì)解析<if><where>等SqlNode
        rootSqlNode.apply(context);
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        
        /*
         * 構(gòu)建 StaticSqlSource丙曙,在此過(guò)程中將 sql 語(yǔ)句中的占位符 #{} 替換為問(wèn)號(hào) ?,
         * 并為每個(gè)占位符構(gòu)建相應(yīng)的 ParameterMapping
         */
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        
        // 調(diào)用 StaticSqlSource 的 getBoundSql 獲取 BoundSql
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        
        // 將 DynamicContext 的 ContextMap 中的內(nèi)容拷貝到 BoundSql 中
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        return boundSql;
    }
}

該方法由數(shù)個(gè)步驟組成其骄,這里總結(jié)一下:

  • 1亏镰、創(chuàng)建 DynamicContext
  • 2、解析 SQL 片段拯爽,并將解析結(jié)果存儲(chǔ)到 DynamicContext 中
  • 3拆挥、解析 SQL 語(yǔ)句,并構(gòu)建 StaticSqlSource
  • 4、調(diào)用 StaticSqlSource 的 getBoundSql 獲取 BoundSql
  • 5纸兔、將 DynamicContext 的 ContextMap 中的內(nèi)容拷貝到 BoundSql
DynamicContext

DynamicContext 是 SQL 語(yǔ)句構(gòu)建的上下文,每個(gè) SQL 片段解析完成后否副,都會(huì)將解析結(jié)果存入 DynamicContext 中汉矿。待所有的 SQL 片段解析完畢后,一條完整的 SQL 語(yǔ)句就會(huì)出現(xiàn)在 DynamicContext 對(duì)象中备禀。

public class DynamicContext {

    public static final String PARAMETER_OBJECT_KEY = "_parameter";
    public static final String DATABASE_ID_KEY = "_databaseId";

    //bindings 則用于存儲(chǔ)一些額外的信息洲拇,比如運(yùn)行時(shí)參數(shù)
    private final ContextMap bindings;
    
    //sqlBuilder 變量用于存放 SQL 片段的解析結(jié)果
    private final StringBuilder sqlBuilder = new StringBuilder();

    public DynamicContext(Configuration configuration, Object parameterObject) {
        // 創(chuàng)建 ContextMap,并將運(yùn)行時(shí)參數(shù)放入ContextMap中
        if (parameterObject != null && !(parameterObject instanceof Map)) {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            bindings = new ContextMap(metaObject);
        } else {
            bindings = new ContextMap(null);
        }
        
        // 存放運(yùn)行時(shí)參數(shù) parameterObject 以及 databaseId
        bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
        bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
    }

    public void bind(String name, Object value) {
        bindings.put(name, value);
    }

    //拼接Sql片段
    public void appendSql(String sql) {
        sqlBuilder.append(sql);
        sqlBuilder.append(" ");
    }

    //得到sql字符串
    public String getSql() {
        return sqlBuilder.toString().trim();
    }

    //繼承HashMap
    static class ContextMap extends HashMap<String, Object> {

        private MetaObject parameterMetaObject;
        
        public ContextMap(MetaObject parameterMetaObject) {
            this.parameterMetaObject = parameterMetaObject;
        }

        @Override
        public Object get(Object key) {
            String strKey = (String) key;
            // 檢查是否包含 strKey,若包含則直接返回
            if (super.containsKey(strKey)) {
                return super.get(strKey);
            }

            if (parameterMetaObject != null) {
                // issue #61 do not modify the context when reading
                // 從運(yùn)行時(shí)參數(shù)中查找結(jié)果曲尸,這里會(huì)在${name}解析時(shí)赋续,通過(guò)name獲取運(yùn)行時(shí)參數(shù)值,替換掉${name}字符串
                return parameterMetaObject.getValue(strKey);
            }

            return null;
        }
    }
    
    // 省略部分代碼

}  
解析 SQL 片段

接著我們來(lái)看看解析SQL片段的邏輯

rootSqlNode.apply(context);

對(duì)于一個(gè)包含了 ${} 占位符另患,或 <if>纽乱、<where> 等標(biāo)簽的 SQL,在解析的過(guò)程中昆箕,會(huì)被分解成多個(gè)片段鸦列。每個(gè)片段都有對(duì)應(yīng)的類型,每種類型的片段都有不同的解析邏輯鹏倘。在源碼中薯嗤,片段這個(gè)概念等價(jià)于 sql 節(jié)點(diǎn),即 SqlNode纤泵。

StaticTextSqlNode 用于存儲(chǔ)靜態(tài)文本骆姐,TextSqlNode 用于存儲(chǔ)帶有 ${} 占位符的文本,IfSqlNode 則用于存儲(chǔ) <if> 節(jié)點(diǎn)的內(nèi)容捏题。MixedSqlNode 內(nèi)部維護(hù)了一個(gè) SqlNode 集合玻褪,用于存儲(chǔ)各種各樣的 SqlNode。接下來(lái)涉馅,我將會(huì)對(duì) MixedSqlNode 归园、StaticTextSqlNode、TextSqlNode稚矿、IfSqlNode庸诱、WhereSqlNode 以及 TrimSqlNode 等進(jìn)行分析

public class MixedSqlNode implements SqlNode {
    private final List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 遍歷 SqlNode 集合
        for (SqlNode sqlNode : contents) {
            // 調(diào)用 salNode 對(duì)象本身的 apply 方法解析 sql
            sqlNode.apply(context);
        }
        return true;
    }
}

MixedSqlNode 可以看做是 SqlNode 實(shí)現(xiàn)類對(duì)象的容器,凡是實(shí)現(xiàn)了 SqlNode 接口的類都可以存儲(chǔ)到 MixedSqlNode 中晤揣,包括它自己桥爽。MixedSqlNode 解析方法 apply 邏輯比較簡(jiǎn)單,即遍歷 SqlNode 集合昧识,并調(diào)用其他 SqlNode實(shí)現(xiàn)類對(duì)象的 apply 方法解析 sql钠四。

StaticTextSqlNode
public class StaticTextSqlNode implements SqlNode {
    private final String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }

    @Override
    public boolean apply(DynamicContext context) {
        //直接拼接當(dāng)前sql片段的文本到DynamicContext的sqlBuilder中
        context.appendSql(text);
        return true;
    }

}

StaticTextSqlNode 用于存儲(chǔ)靜態(tài)文本,直接將其存儲(chǔ)的 SQL 的文本值拼接到 DynamicContext 的sqlBuilder中即可。下面分析一下 TextSqlNode缀去。

TextSqlNode
public class TextSqlNode implements SqlNode {
    private final String text;
    private final Pattern injectionFilter;

    @Override
    public boolean apply(DynamicContext context) {
        // 創(chuàng)建 ${} 占位符解析器
        GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
        
        // 解析 ${} 占位符侣灶,通過(guò)ONGL 從用戶傳入的參數(shù)中獲取結(jié)果,替換text中的${} 占位符
        // 并將解析結(jié)果的文本拼接到DynamicContext的sqlBuilder中
        context.appendSql(parser.parse(text));
        return true;
    }

    private GenericTokenParser createParser(TokenHandler handler) {
        // 創(chuàng)建占位符解析器
        return new GenericTokenParser("${", "}", handler);
    }

    private static class BindingTokenParser implements TokenHandler {

        private DynamicContext context;
        private Pattern injectionFilter;

        public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
            this.context = context;
            this.injectionFilter = injectionFilter;
        }

        @Override
        public String handleToken(String content) {
            Object parameter = context.getBindings().get("_parameter");
            if (parameter == null) {
                context.getBindings().put("value", null);
            } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                context.getBindings().put("value", parameter);
            }
            
            // 通過(guò) ONGL 從用戶傳入的參數(shù)中獲取結(jié)果
            Object value = OgnlCache.getValue(content, context.getBindings());
            String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
            
            // 通過(guò)正則表達(dá)式檢測(cè) srtValue 有效性
            checkInjection(srtValue);
            return srtValue;
        }
    }
}   

GenericTokenParser 是一個(gè)通用的標(biāo)記解析器缕碎,用于解析形如 {name}褥影,#{id} 等標(biāo)記。此時(shí)是解析{name}的形式咏雌,從運(yùn)行時(shí)參數(shù)的Map中獲取到key為name的值凡怎,直接用運(yùn)行時(shí)參數(shù)替換掉 ${name}字符串,將替換后的text字符串拼接到DynamicContext的sqlBuilder中

舉個(gè)例子吧赊抖,比喻我們有如下SQL

SELECT * FROM user WHERE name = '${name}' and id= ${id}

假如我們傳的參數(shù) Map中name值為 chenhao,id為1统倒,那么該 SQL 最終會(huì)被解析成如下的結(jié)果:

SELECT * FROM user WHERE name = 'chenhao'; DROP TABLE user;#'

由于傳入的參數(shù)沒(méi)有經(jīng)過(guò)轉(zhuǎn)義,最終導(dǎo)致了一條 SQL 被惡意參數(shù)拼接成了兩條 SQL氛雪。這就是為什么我們不應(yīng)該在 SQL 語(yǔ)句中是用 ${} 占位符房匆,風(fēng)險(xiǎn)太大。接著我們來(lái)看看IfSqlNode

IfSqlNode
public class IfSqlNode implements SqlNode {
    private final ExpressionEvaluator evaluator;
    private final String test;
    private final SqlNode contents;

    public IfSqlNode(SqlNode contents, String test) {
        this.test = test;
        this.contents = contents;
        this.evaluator = new ExpressionEvaluator();
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 通過(guò) ONGL 評(píng)估 test 表達(dá)式的結(jié)果
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            // 若 test 表達(dá)式中的條件成立注暗,則調(diào)用其子節(jié)點(diǎn)節(jié)點(diǎn)的 apply 方法進(jìn)行解析
            // 如果是靜態(tài)SQL節(jié)點(diǎn)坛缕,則會(huì)直接拼接到DynamicContext中
            contents.apply(context);
            return true;
        }
        return false;
    }

}

IfSqlNode 對(duì)應(yīng)的是 <if test='xxx'> 節(jié)點(diǎn),首先是通過(guò) ONGL 檢測(cè) test 表達(dá)式是否為 true捆昏,如果為 true赚楚,則調(diào)用其子節(jié)點(diǎn)的 apply 方法繼續(xù)進(jìn)行解析。如果子節(jié)點(diǎn)是靜態(tài)SQL節(jié)點(diǎn)骗卜,則子節(jié)點(diǎn)的文本值會(huì)直接拼接到DynamicContext中

好了宠页,其他的SqlNode我就不一一分析了,大家有興趣的可以去看看

解析 #{} 占位符

經(jīng)過(guò)前面的解析寇仓,我們已經(jīng)能從 DynamicContext 獲取到完整的 SQL 語(yǔ)句了举户。但這并不意味著解析過(guò)程就結(jié)束了,因?yàn)楫?dāng)前的 SQL 語(yǔ)句中還有一種占位符沒(méi)有處理遍烦,即 #{}俭嘁。與 ${} 占位符的處理方式不同,MyBatis 并不會(huì)直接將 #{} 占位符替換為相應(yīng)的參數(shù)值服猪,而是將其替換成供填?。其解析是在如下代碼中實(shí)現(xiàn)的

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

我們看到將前面解析過(guò)的sql字符串和運(yùn)行時(shí)參數(shù)的Map作為參數(shù)罢猪,我們來(lái)看看parse方法

public class SqlSourceBuilder extends BaseBuilder {

    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        // 創(chuàng)建 #{} 占位符處理器
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        
        // 創(chuàng)建 #{} 占位符解析器
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        
        // 解析 #{} 占位符近她,并返回解析結(jié)果字符串
        String sql = parser.parse(originalSql);
        
        // 封裝解析結(jié)果到 StaticSqlSource 中,并返回,因?yàn)樗械膭?dòng)態(tài)參數(shù)都已經(jīng)解析了膳帕,可以封裝成一個(gè)靜態(tài)的SqlSource
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }
    
    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

        private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
        private Class<?> parameterType;
        private MetaObject metaParameters;

        public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
            super(configuration);
            this.parameterType = parameterType;
            this.metaParameters = configuration.newMetaObject(additionalParameters);
        }

        public List<ParameterMapping> getParameterMappings() {
            return parameterMappings;
        }

        @Override
        public String handleToken(String content) {
            // 獲取 content 的對(duì)應(yīng)的 ParameterMapping
            parameterMappings.add(buildParameterMapping(content));
            // 返回 ?
            return "?";
        }
    }
}

我們看到將Sql中的 #{} 占位符替換成"?"粘捎,并且將對(duì)應(yīng)的參數(shù)轉(zhuǎn)化成ParameterMapping 對(duì)象,通過(guò)buildParameterMapping 完成,最后創(chuàng)建一個(gè)StaticSqlSource,將sql字符串和ParameterMappings為參數(shù)傳入攒磨,返回這個(gè)StaticSqlSource

public class SqlSourceBuilder extends BaseBuilder {
    
    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

        private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
        private Class<?> parameterType;
        private MetaObject metaParameters;


        /*
         * 將#{xxx} 占位符中的內(nèi)容解析成 Map泳桦。
         *   #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
         *      上面占位符中的內(nèi)容最終會(huì)被解析成如下的結(jié)果:
         *  {
         *      "property": "age",
         *      "typeHandler": "MyTypeHandler", 
         *      "jdbcType": "NUMERIC", 
         *      "javaType": "int"
         *  }
         */
        private ParameterMapping buildParameterMapping(String content) {
            Map<String, String> propertiesMap = parseParameterMapping(content);
            String property = propertiesMap.get("property");
            Class<?> propertyType;
            
            // metaParameters 為 DynamicContext 成員變量 bindings 的元信息對(duì)象
            if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
                propertyType = metaParameters.getGetterType(property);
                
            /*
             * parameterType 是運(yùn)行時(shí)參數(shù)的類型。如果用戶傳入的是單個(gè)參數(shù)娩缰,比如 Employe 對(duì)象蓬痒,此時(shí) 
             * parameterType 為 Employe.class。如果用戶傳入的多個(gè)參數(shù)漆羔,比如 [id = 1, author = "chenhao"],
             * MyBatis 會(huì)使用 ParamMap 封裝這些參數(shù)狱掂,此時(shí) parameterType 為 ParamMap.class演痒。
             */
            } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
                propertyType = parameterType;
            } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
                propertyType = java.sql.ResultSet.class;
            } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
                propertyType = Object.class;
            } else {
            
            /*
             * 代碼邏輯走到此分支中,表明 parameterType 是一個(gè)自定義的類趋惨,
             * 比如 Employe鸟顺,此時(shí)為該類創(chuàng)建一個(gè)元信息對(duì)象
             */
            MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
            // 檢測(cè)參數(shù)對(duì)象有沒(méi)有與 property 想對(duì)應(yīng)的 getter 方法
            if (metaClass.hasGetter(property)) {
                // 獲取成員變量的類型
                propertyType = metaClass.getGetterType(property);
            } else {
                propertyType = Object.class;
            }
            }
            ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
            
            // 將 propertyType 賦值給 javaType
            Class<?> javaType = propertyType;
            String typeHandlerAlias = null;
            
            // 遍歷 propertiesMap
            for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
                String name = entry.getKey();
                String value = entry.getValue();
                if ("javaType".equals(name)) {
                    // 如果用戶明確配置了 javaType,則以用戶的配置為準(zhǔn)
                    javaType = resolveClass(value);
                    builder.javaType(javaType);
                } else if ("jdbcType".equals(name)) {
                    // 解析 jdbcType
                    builder.jdbcType(resolveJdbcType(value));
                } else if ("mode".equals(name)) {
                    builder.mode(resolveParameterMode(value));
                } else if ("numericScale".equals(name)) {
                    builder.numericScale(Integer.valueOf(value));
                } else if ("resultMap".equals(name)) {
                    builder.resultMapId(value);
                } else if ("typeHandler".equals(name)) {
                    typeHandlerAlias = value;
                } else if ("jdbcTypeName".equals(name)) {
                    builder.jdbcTypeName(value);
                } else if ("property".equals(name)) {
                    // Do Nothing
                } else if ("expression".equals(name)) {
                    throw new BuilderException("Expression based parameters are not supported yet");
                } else {
                    throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + parameterProperties);
                }
            }
            if (typeHandlerAlias != null) {
                builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
            }
            
            // 構(gòu)建 ParameterMapping 對(duì)象
            return builder.build();
        }
    }
}

SQL 中的 #{name, ...} 占位符被替換成了問(wèn)號(hào) ?器虾。#{name, ...} 也被解析成了一個(gè) ParameterMapping 對(duì)象讯嫂。我們?cè)賮?lái)看一下 StaticSqlSource 的創(chuàng)建過(guò)程。如下:

public class StaticSqlSource implements SqlSource {

    private final String sql;
    private final List<ParameterMapping> parameterMappings;
    private final Configuration configuration;

    public StaticSqlSource(Configuration configuration, String sql) {
        this(configuration, sql, null);
    }

    public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.configuration = configuration;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        // 創(chuàng)建 BoundSql 對(duì)象
        return new BoundSql(configuration, sql, parameterMappings, parameterObject);
    }

}

最后我們通過(guò)創(chuàng)建的StaticSqlSource就可以獲取BoundSql對(duì)象了兆沙,并傳入運(yùn)行時(shí)參數(shù)

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

也就是調(diào)用上面創(chuàng)建的StaticSqlSource 中的getBoundSql方法欧芽,這是簡(jiǎn)單的 return new BoundSql(configuration, sql, parameterMappings, parameterObject); ,接著看看BoundSql

public class BoundSql {

    private final String sql;
    private final List<ParameterMapping> parameterMappings;
    private final Object parameterObject;
    private final Map<String, Object> additionalParameters;
    private final MetaObject metaParameters;

    public BoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings, Object parameterObject) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.parameterObject = parameterObject;
        this.additionalParameters = new HashMap<String, Object>();
        this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public String getSql() {
        return sql;
    }
    
    //略......
}

我們看到只是做簡(jiǎn)單的賦值葛圃。BoundSql中包含了sql千扔,#{}解析成的parameterMappings,還有運(yùn)行時(shí)參數(shù)parameterObject库正。好了曲楚,SQL解析我們就介紹這么多。我們先回顧一下我們代碼是從哪里開(kāi)始的

CachingExecutor
public class CachingExecutor implements Executor {

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        // 獲取 BoundSql
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        
        // 創(chuàng)建 CacheKey
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        
        // 調(diào)用重載方法
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

如上褥符,我們剛才都是分析的第三行代碼龙誊,獲取到了BoundSql,CacheKey 和二級(jí)緩存有關(guān)喷楣,我們留在下一篇文章單獨(dú)來(lái)講趟大,接著我們看第七行重載方法 query

public class CachingExecutor implements Executor {

    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
        // 從 MappedStatement 中獲取緩存
        Cache cache = ms.getCache();
        
        // 若映射文件中未配置緩存或參照緩存,此時(shí) cache = null
        if (cache != null) {
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    // 若緩存未命中抡蛙,則調(diào)用被裝飾類的 query 方法护昧,也就是SimpleExecutor的query方法
                    list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        
        // 調(diào)用被裝飾類的 query 方法,這里的delegate我們知道應(yīng)該是SimpleExecutor
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

上面的代碼涉及到了二級(jí)緩存,若二級(jí)緩存為空粗截,或未命中惋耙,則調(diào)用被裝飾類的 query 方法。被裝飾類為SimpleExecutor,而SimpleExecutor繼承BaseExecutor绽榛,那我們來(lái)看看 BaseExecutor 的query方法

BaseExecutor.query
public abstract class BaseExecutor implements Executor {

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            queryStack++;
            // 從一級(jí)緩存中獲取緩存項(xiàng)湿酸,一級(jí)緩存我們也下一篇文章單獨(dú)講
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // 一級(jí)緩存未命中,則從數(shù)據(jù)庫(kù)中查詢
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }
}

從一級(jí)緩存中查找查詢結(jié)果灭美。若緩存未命中推溃,再向數(shù)據(jù)庫(kù)進(jìn)行查詢。至此我們明白了一級(jí)二級(jí)緩存的大概思路届腐,先從二級(jí)緩存中查找铁坎,若未命中二級(jí)緩存,再?gòu)囊患?jí)緩存中查找犁苏,若未命中一級(jí)緩存硬萍,再?gòu)臄?shù)據(jù)庫(kù)查詢數(shù)據(jù),那我們來(lái)看看是怎么從數(shù)據(jù)庫(kù)查詢的

BaseExecutor.queryFromDatabase
public abstract class BaseExecutor implements Executor {

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        // 向緩存中存儲(chǔ)一個(gè)占位符
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            // 調(diào)用 doQuery 進(jìn)行查詢
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            // 移除占位符
            localCache.removeObject(key);
        }
        // 緩存查詢結(jié)果
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }

}

調(diào)用了doQuery方法進(jìn)行查詢围详,最后將查詢結(jié)果放入一級(jí)緩存朴乖,我們來(lái)看看doQuery,在SimpleExecutor中

SimpleExecutor
public abstract class BaseExecutor implements Executor {

  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

}


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();
            // 創(chuàng)建 StatementHandler
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            // 創(chuàng)建 Statement
            stmt = prepareStatement(handler, ms.getStatementLog());
            // 執(zhí)行查詢操作
            return handler.<E>query(stmt, resultHandler);
        } finally {
            // 關(guān)閉 Statement
            closeStatement(stmt);
        }
    }
}

我們先來(lái)看看第一步創(chuàng)建StatementHandler

創(chuàng)建StatementHandler

StatementHandler有什么作用呢?通過(guò)這個(gè)對(duì)象獲取Statement對(duì)象助赞,然后填充運(yùn)行時(shí)參數(shù)买羞,最后調(diào)用query完成查詢。我們來(lái)看看其創(chuàng)建過(guò)程

public class Configuration {

    protected final InterceptorChain interceptorChain = new InterceptorChain();

    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        // 創(chuàng)建具有路由功能的 StatementHandler
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        // 應(yīng)用插件到 StatementHandler 上
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }
}

我們看看RoutingStatementHandler的構(gòu)造方法

public class RoutingStatementHandler implements StatementHandler {

    private final StatementHandler delegate;

    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

        // 根據(jù) StatementType 創(chuàng)建不同的 StatementHandler 
        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());
        }

    }
}

RoutingStatementHandler 的構(gòu)造方法會(huì)根據(jù) MappedStatement 中的 statementType 變量創(chuàng)建不同的 StatementHandler 實(shí)現(xiàn)類雹食。那statementType 是什么呢畜普?我們還要回顧一下MappedStatement 的創(chuàng)建過(guò)程

public final class MappedStatement {

    public static class Builder {
        private MappedStatement mappedStatement = new MappedStatement();

        public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
            mappedStatement.configuration = configuration;
            mappedStatement.id = id;
            mappedStatement.sqlSource = sqlSource;
            
            mappedStatement.statementType = StatementType.PREPARED;
            mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<ParameterMapping>()).build();
            mappedStatement.resultMaps = new ArrayList<ResultMap>();
            mappedStatement.sqlCommandType = sqlCommandType;
            mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
            String logId = id;
            if (configuration.getLogPrefix() != null) {
                logId = configuration.getLogPrefix() + id;
            }
            mappedStatement.statementLog = LogFactory.getLog(logId);
            mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
        }
    }
}

我們看到statementType 的默認(rèn)類型為PREPARED,這里將會(huì)創(chuàng)建PreparedStatementHandler婉徘。

接著我們看下面一行代碼prepareStatement,

創(chuàng)建 Statement

創(chuàng)建 Statement 在 stmt = prepareStatement(handler, ms.getStatementLog());這句代碼漠嵌,那我們跟進(jìn)去看看

public class SimpleExecutor extends BaseExecutor {

    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        // 獲取數(shù)據(jù)庫(kù)連接
        Connection connection = getConnection(statementLog);
        // 創(chuàng)建 Statement,
        stmt = handler.prepare(connection, transaction.getTimeout());
        // 為 Statement 設(shè)置參數(shù)
        handler.parameterize(stmt);
        return stmt;
    }
}

在上面的代碼中我們終于看到了和jdbc相關(guān)的內(nèi)容了盖呼,創(chuàng)建完Statement儒鹿,最后就可以執(zhí)行查詢操作了。由于篇幅的原因几晤,我們留在下一篇文章再來(lái)詳細(xì)講解

參考:
https://www.cnblogs.com/java-chen-hao/p/11754184.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末约炎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蟹瘾,更是在濱河造成了極大的恐慌圾浅,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憾朴,死亡現(xiàn)場(chǎng)離奇詭異狸捕,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)众雷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門灸拍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)做祝,“玉大人,你說(shuō)我怎么就攤上這事鸡岗』旎保” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵轩性,是天一觀的道長(zhǎng)声登。 經(jīng)常有香客問(wèn)我,道長(zhǎng)揣苏,這世上最難降的妖魔是什么悯嗓? 我笑而不...
    開(kāi)封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮卸察,結(jié)果婚禮上绅作,老公的妹妹穿的比我還像新娘。我一直安慰自己蛾派,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布个少。 她就那樣靜靜地躺著洪乍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪夜焦。 梳的紋絲不亂的頭發(fā)上壳澳,一...
    開(kāi)封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音茫经,去河邊找鬼巷波。 笑死,一個(gè)胖子當(dāng)著我的面吹牛卸伞,可吹牛的內(nèi)容都是我干的抹镊。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼荤傲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼垮耳!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起遂黍,我...
    開(kāi)封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤终佛,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后雾家,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體铃彰,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年芯咧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了牙捉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竹揍。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖鹃共,靈堂內(nèi)的尸體忽然破棺而出鬼佣,到底是詐尸還是另有隱情,我是刑警寧澤霜浴,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布晶衷,位于F島的核電站,受9級(jí)特大地震影響阴孟,放射性物質(zhì)發(fā)生泄漏晌纫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一永丝、第九天 我趴在偏房一處隱蔽的房頂上張望锹漱。 院中可真熱鬧,春花似錦慕嚷、人聲如沸哥牍。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嗅辣。三九已至,卻和暖如春挠说,著一層夾襖步出監(jiān)牢的瞬間澡谭,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工损俭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛙奖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓杆兵,卻偏偏與公主長(zhǎng)得像雁仲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子琐脏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

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

  • 久違的晴天伯顶,家長(zhǎng)會(huì)。 家長(zhǎng)大會(huì)開(kāi)好到教室時(shí)骆膝,離放學(xué)已經(jīng)沒(méi)多少時(shí)間了祭衩。班主任說(shuō)已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,528評(píng)論 16 22
  • 今天感恩節(jié)哎阅签,感謝一直在我身邊的親朋好友掐暮。感恩相遇!感恩不離不棄政钟。 中午開(kāi)了第一次的黨會(huì)路克,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,576評(píng)論 0 11
  • 可愛(ài)進(jìn)取樟结,孤獨(dú)成精。努力飛翔精算,天堂翱翔瓢宦。戰(zhàn)爭(zhēng)美好,孤獨(dú)進(jìn)取灰羽。膽大飛翔驮履,成就輝煌。努力進(jìn)取廉嚼,遙望玫镐,和諧家園〉≡耄可愛(ài)游走...
    趙原野閱讀 2,738評(píng)論 1 1
  • 在妖界我有個(gè)名頭叫胡百曉恐似,無(wú)論是何事,只要找到胡百曉即可有解決的辦法傍念。因?yàn)槭侵缓偞蠹乙杂瀭饔灲形摇皟A城百曉”矫夷,...
    貓九0110閱讀 3,274評(píng)論 7 3