Spring系列之JDBC對(duì)不同數(shù)據(jù)庫(kù)異常如何抽象的掸驱?

前言

使用Spring-Jdbc的情況下,在有些場(chǎng)景中没佑,我們需要根據(jù)數(shù)據(jù)庫(kù)報(bào)的異常類型的不同,來(lái)編寫我們的業(yè)務(wù)代碼温赔。比如說(shuō)蛤奢,我們有這樣一段邏輯,如果我們新插入的記錄陶贼,存在唯一約束沖突啤贩,就會(huì)返回給客戶端描述:記錄已存在,請(qǐng)勿重復(fù)操作
代碼一般是這么寫的:

@Resource
private JdbcTemplate jdbcTemplate;
public String testAdd(){
    try {
        jdbcTemplate.execute("INSERT INTO user_info (user_id, user_name, email, nick_name, status, address) VALUES (80002, '張三豐', 'xxx@126.com', '張真人', 1, '武當(dāng)山');");
        return "OK";
    }catch (DuplicateKeyException e){
        return "記錄已存在拜秧,請(qǐng)勿重復(fù)操作";
    }
}

測(cè)試一下:

file

如上圖提示痹屹,并且無(wú)論什么更換什么數(shù)據(jù)庫(kù)(Spring-Jdbc支持的),代碼都不用改動(dòng)

那么Spring-Jdbc是在使用不同數(shù)據(jù)庫(kù)時(shí)枉氮,Spring如何幫我們實(shí)現(xiàn)對(duì)異常的抽象的呢?

代碼實(shí)現(xiàn)

我們來(lái)正向看下代碼:
首先入口JdbcTemplate.execute方法:

public void execute(final String sql) throws DataAccessException {
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Executing SQL statement [" + sql + "]");
    }
    ...
    //實(shí)際執(zhí)行入口
    this.execute(new ExecuteStatementCallback(), true);
}

內(nèi)部方法execute

@Nullable
private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
    Assert.notNull(action, "Callback object must not be null");
    Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
    Statement stmt = null;

    Object var12;
    try {
        stmt = con.createStatement();
        this.applyStatementSettings(stmt);
        T result = action.doInStatement(stmt);
        this.handleWarnings(stmt);
        var12 = result;
    } catch (SQLException var10) {
        String sql = getSql(action);
        JdbcUtils.closeStatement(stmt);
        stmt = null;
        DataSourceUtils.releaseConnection(con, this.getDataSource());
        con = null;
         //SQL出現(xiàn)異常后志衍,在這里進(jìn)行異常轉(zhuǎn)換
        throw this.translateException("StatementCallback", sql, var10);
    } finally {
        if (closeResources) {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(con, this.getDataSource());
        }

    }

    return var12;
}

異常轉(zhuǎn)換方法translateException

protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) {
    //獲取異常轉(zhuǎn)換器,然后根據(jù)數(shù)據(jù)庫(kù)返回碼相關(guān)信息執(zhí)行轉(zhuǎn)換操作
        //轉(zhuǎn)換不成功聊替,也有兜底異常UncategorizedSQLException
    DataAccessException dae = this.getExceptionTranslator().translate(task, sql, ex);
    return (DataAccessException)(dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
}

獲取轉(zhuǎn)換器方法getExceptionTranslator

public SQLExceptionTranslator getExceptionTranslator() {
    //獲取轉(zhuǎn)換器屬性楼肪,如果為空,則生成一個(gè)
    SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
    if (exceptionTranslator != null) {
        return exceptionTranslator;
    } else {
        synchronized(this) {
            SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
            if (exceptionTranslator == null) {
                DataSource dataSource = this.getDataSource();
                //shouldIgnoreXml是一個(gè)標(biāo)記惹悄,就是不通過(guò)xml加載bean春叫,默認(rèn)false
                                if (shouldIgnoreXml) {
                    exceptionTranslator = new SQLExceptionSubclassTranslator();
                } else if (dataSource != null) {
                             //如果DataSource不為空,則生成轉(zhuǎn)換器SQLErrorCodeSQLExceptionTranslator
                    exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
                } else {
                             // 其他情況泣港,生成SQLStateSQLExceptionTranslator轉(zhuǎn)換器
                    exceptionTranslator = new SQLStateSQLExceptionTranslator();
                }

                this.exceptionTranslator = (SQLExceptionTranslator)exceptionTranslator;
            }
            return (SQLExceptionTranslator)exceptionTranslator;
        }
    }
}

轉(zhuǎn)換方法:
因?yàn)槟J(rèn)的轉(zhuǎn)換器是SQLErrorCodeSQLExceptionTranslator暂殖,所以這里調(diào)用SQLErrorCodeSQLExceptionTranslator的doTranslate方法

file

類圖調(diào)用關(guān)系如上,實(shí)際先調(diào)用的是AbstractFallbackSQLExceptionTranslator.translate的方法

@Nullable
public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
    Assert.notNull(ex, "Cannot translate a null SQLException");
        //這里才真正調(diào)用SQLErrorCodeSQLExceptionTranslator.doTranslate方法
    DataAccessException dae = this.doTranslate(task, sql, ex);
    if (dae != null) {
        return dae;
    } else {
            //如果沒有找到響應(yīng)的異常当纱,則調(diào)用其他轉(zhuǎn)換器呛每,輸入遞歸調(diào)用,這里后面說(shuō)
        SQLExceptionTranslator fallback = this.getFallbackTranslator();
        return fallback != null ? fallback.translate(task, sql, ex) : null;
    }
}

實(shí)際轉(zhuǎn)換類SQLErrorCodeSQLExceptionTranslator的方法:

//這里省略了一些無(wú)關(guān)代碼惫东,只保留了核心代碼
//先獲取SQLErrorCodes集合莉给,在根據(jù)返回的SQLException中獲取的ErrorCode進(jìn)行匹配,根據(jù)匹配結(jié)果進(jìn)行返回響應(yīng)的異常
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
    ....
    SQLErrorCodes sqlErrorCodes = this.getSqlErrorCodes();
    
    String errorCode = Integer.toString(ex.getErrorCode());
    ...
    if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
            this.logTranslation(task, sql, sqlEx, false);
            return new DuplicateKeyException(this.buildMessage(task, sql, sqlEx), sqlEx);
    }
    ...
    return null;
}

上面的SQLErrorCodes是一個(gè)錯(cuò)誤碼集合廉沮,但是不是全部數(shù)據(jù)庫(kù)的所有錯(cuò)誤碼集合颓遏,而是只取了相應(yīng)數(shù)據(jù)庫(kù)的錯(cuò)誤碼集合,怎么保證獲取的是當(dāng)前使用的數(shù)據(jù)庫(kù)的錯(cuò)誤碼滞时,而不是其他數(shù)據(jù)庫(kù)的錯(cuò)誤碼呢叁幢?當(dāng)然Spring為我們實(shí)現(xiàn)了,在SQLErrorCodeSQLExceptionTranslator中:

public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator {

    private SingletonSupplier<SQLErrorCodes> sqlErrorCodes;
        //默認(rèn)構(gòu)造方法坪稽,設(shè)置了如果轉(zhuǎn)換失敗曼玩,下一個(gè)轉(zhuǎn)換器是SQLExceptionSubclassTranslator
    public SQLErrorCodeSQLExceptionTranslator() {
        this.setFallbackTranslator(new SQLExceptionSubclassTranslator());
    }
   //前面生成轉(zhuǎn)換器的時(shí)候鳞骤,exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
     //使用的是本構(gòu)造方法,傳入了DataSource黍判,其中有數(shù)據(jù)庫(kù)廠商信息豫尽,本文中是MYSQL
    public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) {
        this();
        this.setDataSource(dataSource);
    }

   //從錯(cuò)誤碼工廠SQLErrorCodesFactory里,獲取和數(shù)據(jù)源對(duì)應(yīng)的廠商的所有錯(cuò)誤碼
    public void setDataSource(DataSource dataSource) {
        this.sqlErrorCodes = SingletonSupplier.of(() -> {
            return SQLErrorCodesFactory.getInstance().resolveErrorCodes(dataSource);
        });
        this.sqlErrorCodes.get();
    }

}

錯(cuò)誤碼工廠SQLErrorCodesFactory的resolveErrorCodes方法:

//既然是工廠顷帖,里面肯定有各種數(shù)據(jù)庫(kù)的錯(cuò)誤碼美旧,本文中使用的是MYSQL,我們看一下實(shí)現(xiàn)邏輯
@Nullable
public SQLErrorCodes resolveErrorCodes(DataSource dataSource) {
    Assert.notNull(dataSource, "DataSource must not be null");
    if (logger.isDebugEnabled()) {
        logger.debug("Looking up default SQLErrorCodes for DataSource [" + this.identify(dataSource) + "]");
    }
    //從緩存中拿MYSQL對(duì)應(yīng)的SQLErrorCodes
    SQLErrorCodes sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource);
    if (sec == null) {
        synchronized(this.dataSourceCache) {
            sec = (SQLErrorCodes)this.dataSourceCache.get(dataSource);
            if (sec == null) {
                try {
                    String name = (String)JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getDatabaseProductName);
                    if (StringUtils.hasLength(name)) {
                        SQLErrorCodes var10000 = this.registerDatabase(dataSource, name);
                        return var10000;
                    }
                } catch (MetaDataAccessException var6) {
                    logger.warn("Error while extracting database name", var6);
                }

                return null;
            }
        }
    }

    if (logger.isDebugEnabled()) {
        logger.debug("SQLErrorCodes found in cache for DataSource [" + this.identify(dataSource) + "]");
    }

    return sec;
}

**緩存dataSourceCache如何生成的贬墩? **

public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName) {
   //根據(jù)數(shù)據(jù)庫(kù)類型名稱(這里是MySQL)榴嗅,獲取錯(cuò)誤碼列表
    SQLErrorCodes sec = this.getErrorCodes(databaseName);
    if (logger.isDebugEnabled()) {
        logger.debug("Caching SQL error codes for DataSource [" + this.identify(dataSource) + "]: database product name is '" + databaseName + "'");
    }

    this.dataSourceCache.put(dataSource, sec);
    return sec;
}

public SQLErrorCodes getErrorCodes(String databaseName) {
        Assert.notNull(databaseName, "Database product name must not be null");
                //從errorCodesMap根據(jù)key=MYSQL獲取SQLErrorCodes
        SQLErrorCodes sec = (SQLErrorCodes)this.errorCodesMap.get(databaseName);
        if (sec == null) {
            Iterator var3 = this.errorCodesMap.values().iterator();

            while(var3.hasNext()) {
                SQLErrorCodes candidate = (SQLErrorCodes)var3.next();
                if (PatternMatchUtils.simpleMatch(candidate.getDatabaseProductNames(), databaseName)) {
                    sec = candidate;
                    break;
                }
            }
        }

        if (sec != null) {
            this.checkCustomTranslatorRegistry(databaseName, sec);
            if (logger.isDebugEnabled()) {
                logger.debug("SQL error codes for '" + databaseName + "' found");
            }

            return sec;
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("SQL error codes for '" + databaseName + "' not found");
            }

            return new SQLErrorCodes();
        }
    }
        
        
//SQLErrorCodesFactory構(gòu)造方法中,生成的errorCodesMap陶舞,map的內(nèi)容來(lái)自org/springframework/jdbc/support/sql-error-codes.xml文件     
protected SQLErrorCodesFactory() {
        Map errorCodes;
        try {
            DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
            lbf.setBeanClassLoader(this.getClass().getClassLoader());
            XmlBeanDefinitionReader bdr = new XmlBeanDefinitionReader(lbf);
            Resource resource = this.loadResource("org/springframework/jdbc/support/sql-error-codes.xml");
            if (resource != null && resource.exists()) {
                bdr.loadBeanDefinitions(resource);
            } else {
                logger.info("Default sql-error-codes.xml not found (should be included in spring-jdbc jar)");
            }

            resource = this.loadResource("sql-error-codes.xml");
            if (resource != null && resource.exists()) {
                bdr.loadBeanDefinitions(resource);
                logger.debug("Found custom sql-error-codes.xml file at the root of the classpath");
            }

            errorCodes = lbf.getBeansOfType(SQLErrorCodes.class, true, false);
            if (logger.isTraceEnabled()) {
                logger.trace("SQLErrorCodes loaded: " + errorCodes.keySet());
            }
        } catch (BeansException var5) {
            logger.warn("Error loading SQL error codes from config file", var5);
            errorCodes = Collections.emptyMap();
        }

        this.errorCodesMap = errorCodes;
}

sql-error-codes.xml文件中配置了各個(gè)數(shù)據(jù)庫(kù)的主要的錯(cuò)誤碼
這里列舉了MYSQL部分嗽测,當(dāng)然還有其他部分,我們可以看到唯一性約束錯(cuò)誤碼是1062肿孵,就可以翻譯成DuplicateKeyException異常了

<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
        <property name="databaseProductNames">
            <list>
                <value>MySQL</value>
                <value>MariaDB</value>
            </list>
        </property>
        <property name="badSqlGrammarCodes">
            <value>1054,1064,1146</value>
        </property>
        <property name="duplicateKeyCodes">
            <value>1062</value>
        </property>
        <property name="dataIntegrityViolationCodes">
            <value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value>
        </property>
        <property name="dataAccessResourceFailureCodes">
            <value>1</value>
        </property>
        <property name="cannotAcquireLockCodes">
            <value>1205,3572</value>
        </property>
        <property name="deadlockLoserCodes">
            <value>1213</value>
        </property>
    </bean>

你已經(jīng)看到唠粥,比如上面的錯(cuò)誤碼值列舉了一部分,如果出現(xiàn)了一個(gè)不在其中的錯(cuò)誤碼肯定是匹配不到颁井,Spring當(dāng)然能想到這種情況了

   /**
     *@公-眾-號(hào):程序員阿牛
     *在AbstractFallbackSQLExceptionTranslator中厅贪,看到如果查找失敗會(huì)獲取下一個(gè)后續(xù)轉(zhuǎn)換器
     */
    @Nullable
    public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
        Assert.notNull(ex, "Cannot translate a null SQLException");
        DataAccessException dae = this.doTranslate(task, sql, ex);
        if (dae != null) {
            return dae;
        } else {
            SQLExceptionTranslator fallback = this.getFallbackTranslator();
            return fallback != null ? fallback.translate(task, sql, ex) : null;
        }
    }

SQLErrorCodeSQLExceptionTranslator的后置轉(zhuǎn)換器是什么?

//構(gòu)造方法中已經(jīng)指定雅宾,SQLExceptionSubclassTranslator
public SQLErrorCodeSQLExceptionTranslator() {
   this.setFallbackTranslator(new SQLExceptionSubclassTranslator());
}

SQLExceptionSubclassTranslator的轉(zhuǎn)換方法邏輯如下:

/**
*@公-眾-號(hào):程序員阿牛
*可以看出實(shí)際按照子類類型來(lái)判斷养涮,返回相應(yīng)的錯(cuò)誤類,如果匹配不到眉抬,則找到下一個(gè)處理器贯吓,這里的處理其我們可以根據(jù)構(gòu)造方法青松找到*SQLStateSQLExceptionTranslator
*/
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
    if (ex instanceof SQLTransientException) {
        if (ex instanceof SQLTransientConnectionException) {
            return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex);
        }

        if (ex instanceof SQLTransactionRollbackException) {
            return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex);
        }

        if (ex instanceof SQLTimeoutException) {
            return new QueryTimeoutException(this.buildMessage(task, sql, ex), ex);
        }
    } else if (ex instanceof SQLNonTransientException) {
        if (ex instanceof SQLNonTransientConnectionException) {
            return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex);
        }

        if (ex instanceof SQLDataException) {
            return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
        }

        if (ex instanceof SQLIntegrityConstraintViolationException) {
            return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
        }

        if (ex instanceof SQLInvalidAuthorizationSpecException) {
            return new PermissionDeniedDataAccessException(this.buildMessage(task, sql, ex), ex);
        }

        if (ex instanceof SQLSyntaxErrorException) {
            return new BadSqlGrammarException(task, sql != null ? sql : "", ex);
        }

        if (ex instanceof SQLFeatureNotSupportedException) {
            return new InvalidDataAccessApiUsageException(this.buildMessage(task, sql, ex), ex);
        }
    } else if (ex instanceof SQLRecoverableException) {
        return new RecoverableDataAccessException(this.buildMessage(task, sql, ex), ex);
    }

    return null;
}

SQLStateSQLExceptionTranslator的轉(zhuǎn)換方法:

/**
*@公-眾-號(hào):程序員阿牛
*可以看出根據(jù)SQLState的前兩位來(lái)判斷異常,根據(jù)匹配結(jié)果返回相應(yīng)的異常信息  
*/
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {
    String sqlState = this.getSqlState(ex);
    if (sqlState != null && sqlState.length() >= 2) {
        String classCode = sqlState.substring(0, 2);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'");
        }

        if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) {
            return new BadSqlGrammarException(task, sql != null ? sql : "", ex);
        }

        if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {
            return new DataIntegrityViolationException(this.buildMessage(task, sql, ex), ex);
        }

        if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {
            return new DataAccessResourceFailureException(this.buildMessage(task, sql, ex), ex);
        }

        if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {
            return new TransientDataAccessResourceException(this.buildMessage(task, sql, ex), ex);
        }

        if (CONCURRENCY_FAILURE_CODES.contains(classCode)) {
            return new ConcurrencyFailureException(this.buildMessage(task, sql, ex), ex);
        }
    }

    return ex.getClass().getName().contains("Timeout") ? new QueryTimeoutException(this.buildMessage(task, sql, ex), ex) : null;
}

為什么SQLState可以得出錯(cuò)誤類型蜀变?

因?yàn)閿?shù)據(jù)庫(kù)是根據(jù) X/Open 和 SQL Access Group SQL CAE 規(guī)范 (1992) 所進(jìn)行的定義悄谐,SQLERROR 返回 SQLSTATE 值。SQLSTATE 值是包含五個(gè)字符的字符串 库北。五個(gè)字符包含數(shù)值或者大寫字母爬舰, 代表各種錯(cuò)誤或者警告條件的代碼。SQLSTATE 有個(gè)層次化的模式:頭兩個(gè)字符標(biāo)識(shí)條件的通常表示錯(cuò)誤條件的類別寒瓦, 后三個(gè)字符表示在該通用類中的子類情屹。成功的狀態(tài)是由 00000 標(biāo)識(shí)的。SQLSTATE 代碼在大多數(shù)地方都是定義在 SQL 標(biāo)準(zhǔn)里

處理流程圖

file

用到了哪些設(shè)計(jì)模式杂腰?

組合模式

file

通過(guò)上圖大家有沒有發(fā)現(xiàn)三個(gè)實(shí)現(xiàn)類之間的關(guān)系—組合關(guān)系垃你,組合關(guān)系在父類AbstractFallbackSQLExceptionTranslator中變成了遞歸調(diào)用,這里充滿了智慧(Composite設(shè)計(jì)模式)。

單例模式

在SQLErrorCodesFactory(單例模式)

策略模式

根據(jù)數(shù)據(jù)庫(kù)的不同惜颇,獲取不同的errorcodes集合

---------------------END---------------------
關(guān)注:程序員阿牛皆刺,Spring系列更多文章,為你呈現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凌摄,一起剝皮案震驚了整個(gè)濱河市羡蛾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锨亏,老刑警劉巖林说,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異屯伞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)豪直,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門劣摇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人弓乙,你說(shuō)我怎么就攤上這事末融。” “怎么了暇韧?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵勾习,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我懈玻,道長(zhǎng)巧婶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任涂乌,我火速辦了婚禮艺栈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘湾盒。我一直安慰自己湿右,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布罚勾。 她就那樣靜靜地躺著毅人,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尖殃。 梳的紋絲不亂的頭發(fā)上丈莺,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音分衫,去河邊找鬼场刑。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的牵现。 我是一名探鬼主播铐懊,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瞎疼!你這毒婦竟也來(lái)了科乎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贼急,失蹤者是張志新(化名)和其女友劉穎茅茂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體太抓,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡空闲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了走敌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碴倾。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖掉丽,靈堂內(nèi)的尸體忽然破棺而出跌榔,到底是詐尸還是另有隱情,我是刑警寧澤捶障,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布僧须,位于F島的核電站,受9級(jí)特大地震影響项炼,放射性物質(zhì)發(fā)生泄漏担平。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一锭部、第九天 我趴在偏房一處隱蔽的房頂上張望驱闷。 院中可真熱鬧,春花似錦空免、人聲如沸空另。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扼菠。三九已至,卻和暖如春坝咐,著一層夾襖步出監(jiān)牢的瞬間循榆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工墨坚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留秧饮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像盗尸,于是被迫代替她去往敵國(guó)和親柑船。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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