springboot配置多數(shù)據(jù)源后mybatis攔截器失效

關(guān)鍵字:springcloud弥咪、mybatis繁扎、多數(shù)據(jù)源負(fù)載均衡悠垛、攔截器動態(tài)分頁

配置文件是通過springcloudconfig遠(yuǎn)程分布式配置但荤。采用阿里Druid數(shù)據(jù)源。并支持一主多從的讀寫分離彼城。分頁組件通過攔截器攔截帶有page后綴的方法名诅蝶,動態(tài)的設(shè)置total總數(shù)退个。
1. 解析配置文件初始化數(shù)據(jù)源
@Configuration
public class DataSourceConfiguration {
    /**
     * 數(shù)據(jù)源類型
     */
    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    /**
     * 主數(shù)據(jù)源配置
     *
     * @return
     */
    @Bean(name = "masterDataSource", destroyMethod = "close")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource masterDataSource() {
        DataSource source = DataSourceBuilder.create().type(dataSourceType).build();
        return source;
    }

    /**
     * 從數(shù)據(jù)源配置
     *
     * @return
     */
    @Bean(name = "slaveDataSource0")
    @ConfigurationProperties(prefix = "spring.slave0")
    public DataSource slaveDataSource0() {
        DataSource source = DataSourceBuilder.create().type(dataSourceType).build();
        return source;
    }

    /**
     * 從數(shù)據(jù)源集合
     *
     * @return
     */
    @Bean(name = "slaveDataSources")
    public List<DataSource> slaveDataSources() {
        List<DataSource> slaveDataSources = new ArrayList();
        slaveDataSources.add(slaveDataSource0());
        return slaveDataSources;
    }
}
2. 定義數(shù)據(jù)源枚舉類型
public enum DataSourceType {
    master("master", "master"), slave("slave", "slave");
    private String type;

    private String name;

    DataSourceType(String type, String name) {
        this.type = type;
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
3. TheadLocal保存數(shù)據(jù)源類型
public class DataSourceContextHolder {
    private static final ThreadLocal<String> local = new ThreadLocal<String>();

    public static ThreadLocal<String> getLocal() {
        return local;
    }

    public static void slave() {
        local.set(DataSourceType.slave.getType());
    }

    public static void master() {
        local.set(DataSourceType.master.getType());
    }

    public static String getJdbcType() {
        return local.get();
    }

    public static void clearDataSource(){
        local.remove();
    }
}
4. 自定義sqlSessionProxy,并將數(shù)據(jù)源填充到DataSourceRoute
@Configuration
@ConditionalOnClass({EnableTransactionManagement.class})
@Import({DataSourceConfiguration.class})
public class DataSourceSqlSessionFactory {
    private Logger logger = Logger.getLogger(DataSourceSqlSessionFactory.class);

    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;

    @Value("${mybatis.type-aliases-package}")
    private String aliasesPackage;

    @Value("${slave.datasource.number}")
    private int dataSourceNumber;

    @Resource(name = "masterDataSource")
    private DataSource masterDataSource;

    @Resource(name = "slaveDataSources")
    private List<DataSource> slaveDataSources;

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        logger.info("======================= init sqlSessionFactory");
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(roundRobinDataSourceProxy());
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocations));
        sqlSessionFactoryBean.setTypeAliasesPackage(aliasesPackage);
        sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "roundRobinDataSourceProxy")
    public AbstractRoutingDataSource roundRobinDataSourceProxy() {
        logger.info("======================= init robinDataSourceProxy");
        DataSourceRoute proxy = new DataSourceRoute(dataSourceNumber);
        Map<Object, Object> targetDataSources = new HashMap();
        targetDataSources.put(DataSourceType.master.getType(), masterDataSource);
        if(null != slaveDataSources) {
            for(int i=0; i<slaveDataSources.size(); i++){
                targetDataSources.put(i, slaveDataSources.get(i));
            }
        }
        proxy.setDefaultTargetDataSource(masterDataSource);
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }
}
5. 自定義路由
public class DataSourceRoute extends AbstractRoutingDataSource {

    private Logger logger = Logger.getLogger(DataSourceRoute.class);

    private final int dataSourceNumber;
    
    public DataSourceRoute(int dataSourceNumber) {
        this.dataSourceNumber = dataSourceNumber;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getJdbcType();
        logger.info("==================== swtich dataSource:" + typeKey);
        if (typeKey.equals(DataSourceType.master.getType())) {
            return DataSourceType.master.getType();
        }else{
            //從數(shù)據(jù)源隨機(jī)分配
            Random random = new Random();
            int slaveDsIndex = random.nextInt(dataSourceNumber);
            return slaveDsIndex;
        }
    }
}
6. 定義切面调炬,dao層定義切面
@Aspect
@Component
public class DataSourceAop {

    private Logger logger = Logger.getLogger(DataSourceAop.class);

    @Before("execution(* com.dbq.iot.mapper..*.get*(..)) || execution(* com.dbq.iot.mapper..*.isExist*(..)) " +
            "|| execution(* com.dbq.iot.mapper..*.select*(..)) || execution(* com.dbq.iot.mapper..*.count*(..)) " +
            "|| execution(* com.dbq.iot.mapper..*.list*(..)) || execution(* com.dbq.iot.mapper..*.query*(..))" +
            "|| execution(* com.dbq.iot.mapper..*.find*(..))|| execution(* com.dbq.iot.mapper..*.search*(..))")
    public void setSlaveDataSourceType(JoinPoint joinPoint) {
        DataSourceContextHolder.slave();
        logger.info("=========slave, method:" + joinPoint.getSignature().getName());
    }

    @Before("execution(* com.dbq.iot.mapper..*.add*(..)) || execution(* com.dbq.iot.mapper..*.del*(..))" +
            "||execution(* com.dbq.iot.mapper..*.upDate*(..)) || execution(* com.dbq.iot.mapper..*.insert*(..))" +
            "||execution(* com.dbq.iot.mapper..*.create*(..)) || execution(* com.dbq.iot.mapper..*.update*(..))" +
            "||execution(* com.dbq.iot.mapper..*.delete*(..)) || execution(* com.dbq.iot.mapper..*.remove*(..))" +
            "||execution(* com.dbq.iot.mapper..*.save*(..)) || execution(* com.dbq.iot.mapper..*.relieve*(..))" +
            "|| execution(* com.dbq.iot.mapper..*.edit*(..))")
    public void setMasterDataSourceType(JoinPoint joinPoint) {
        DataSourceContextHolder.master();
        logger.info("=========master, method:" + joinPoint.getSignature().getName());
    }
}
7. 最后在寫庫增加事務(wù)管理
@Configuration
@Import({DataSourceConfiguration.class})
public class DataSouceTranscation extends DataSourceTransactionManagerAutoConfiguration {

    private Logger logger = Logger.getLogger(DataSouceTranscation.class);

    @Resource(name = "masterDataSource")
    private DataSource masterDataSource;

    /**
     * 配置事務(wù)管理器
     *
     * @return
     */
    @Bean(name = "transactionManager")
    public DataSourceTransactionManager transactionManagers() {
        logger.info("===================== init transactionManager");
        return new DataSourceTransactionManager(masterDataSource);
    }


}
8. 在配置文件中增加數(shù)據(jù)源配置
spring.datasource.name=writedb
spring.datasource.url=jdbc:mysql://192.168.0.1/master?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;failOverReadOnly=false
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.filters=stat
spring.datasource.initialSize=20
spring.datasource.minIdle=20
spring.datasource.maxActive=200
spring.datasource.maxWait=60000

#從庫的數(shù)量
slave.datasource.number=1

spring.slave0.name=readdb
spring.slave0.url=jdbc:mysql://192.168.0.2/slave?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;failOverReadOnly=false
spring.slave0.username=root
spring.slave0.password=1234
spring.slave0.type=com.alibaba.druid.pool.DruidDataSource
spring.slave0.driver-class-name=com.mysql.jdbc.Driver
spring.slave0.filters=stat
spring.slave0.initialSize=20
spring.slave0.minIdle=20
spring.slave0.maxActive=200
spring.slave0.maxWait=60000
這樣就實(shí)現(xiàn)了在springcloud框架下的讀寫分離语盈,并且支持多個(gè)從庫的負(fù)載均衡(簡單的通過隨機(jī)分配,也有網(wǎng)友通過算法實(shí)現(xiàn)平均分配缰泡,具體做法是通過一個(gè)線程安全的自增長Integer類型刀荒,取余實(shí)現(xiàn)。個(gè)人覺得沒大必要棘钞。如果有大神有更好的方法可以一起探討缠借。)
Mabatis分頁配置可通過dao層的攔截器對特定方法進(jìn)行攔截,攔截后添加自己的邏輯代碼宜猜,比如計(jì)算total等泼返,具體代碼如下(參考了網(wǎng)友的代碼,主要是通過@Intercepts注解):
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PageInterceptor implements Interceptor {
    private static final Log logger = LogFactory.getLog(PageInterceptor.class);
    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private static String defaultDialect = "mysql"; // 數(shù)據(jù)庫類型(默認(rèn)為mysql)
    private static String defaultPageSqlId = ".*Page$"; // 需要攔截的ID(正則匹配)
    private String dialect = ""; // 數(shù)據(jù)庫類型(默認(rèn)為mysql)
    private String pageSqlId = ""; // 需要攔截的ID(正則匹配)

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,
                DEFAULT_OBJECT_WRAPPER_FACTORY,DEFAULT_REFLECTOR_FACTORY);
        // 分離代理對象鏈(由于目標(biāo)類可能被多個(gè)攔截器攔截姨拥,從而形成多次代理绅喉,通過下面的兩次循環(huán)可以分離出最原始的的目標(biāo)類)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY,DEFAULT_REFLECTOR_FACTORY);
        }
        // 分離最后一個(gè)代理對象的目標(biāo)類
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY,DEFAULT_REFLECTOR_FACTORY);
        }
        Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
        if (null == dialect || "".equals(dialect)) {
            logger.warn("Property dialect is not setted,use default 'mysql' ");
            dialect = defaultDialect;
        }
        if (null == pageSqlId || "".equals(pageSqlId)) {
            logger.warn("Property pageSqlId is not setted,use default '.*Page$' ");
            pageSqlId = defaultPageSqlId;
        }
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        // 只重寫需要分頁的sql語句。通過MappedStatement的ID匹配叫乌,默認(rèn)重寫以Page結(jié)尾的MappedStatement的sql
        if (mappedStatement.getId().matches(pageSqlId)) {
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
            Object parameterObject = boundSql.getParameterObject();
            if (parameterObject == null) {
                throw new NullPointerException("parameterObject is null!");
            } else {
                PageParameter page = (PageParameter) metaStatementHandler
                        .getValue("delegate.boundSql.parameterObject.page");
                String sql = boundSql.getSql();
                // 重寫sql
                String pageSql = buildPageSql(sql, page);
                metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
                metaStatementHandler.setValue("delegate.rowBounds.offset", RowBounds.NO_ROW_OFFSET);
                metaStatementHandler.setValue("delegate.rowBounds.limit", RowBounds.NO_ROW_LIMIT);
                Connection connection = (Connection) invocation.getArgs()[0];
                // 重設(shè)分頁參數(shù)里的總頁數(shù)等
                setPageParameter(sql, connection, mappedStatement, boundSql, page);
            }
        }
        // 將執(zhí)行權(quán)交給下一個(gè)攔截器
        return invocation.proceed();
    }

    /**
     * @param sql
     * @param connection
     * @param mappedStatement
     * @param boundSql
     * @param page
     */
    private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
                                  BoundSql boundSql, PageParameter page) {
        // 記錄總記錄數(shù)
        String countSql = "select count(0) from (" + sql + ") as total";
        PreparedStatement countStmt = null;
        ResultSet rs = null;
        try {
            countStmt = connection.prepareStatement(countSql);
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());

            Field metaParamsField = ReflectUtil.getFieldByFieldName(boundSql, "metaParameters");
            if (metaParamsField != null) {
                try {
                    MetaObject mo = (MetaObject) ReflectUtil.getValueByFieldName(boundSql, "metaParameters");
                    ReflectUtil.setValueByFieldName(countBS, "metaParameters", mo);
                } catch (SecurityException | NoSuchFieldException | IllegalArgumentException
                        | IllegalAccessException e) {
                    // TODO Auto-generated catch block
                     logger.error("Ignore this exception", e);
                }
            }
            Field additionalField = ReflectUtil.getFieldByFieldName(boundSql, "additionalParameters");
            if (additionalField != null) {
                try {
                    Map<String, Object> map = (Map<String, Object>) ReflectUtil.getValueByFieldName(boundSql, "additionalParameters");
                    ReflectUtil.setValueByFieldName(countBS, "additionalParameters", map);
                } catch (SecurityException | NoSuchFieldException | IllegalArgumentException
                        | IllegalAccessException e) {
                    // TODO Auto-generated catch block
                    logger.error("Ignore this exception", e);
                }
            }

            setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
            rs = countStmt.executeQuery();
            int totalCount = 0;
            if (rs.next()) {
                totalCount = rs.getInt(1);
            }
            page.setTotalCount(totalCount);
            int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
            page.setTotalPage(totalPage);

        } catch (SQLException e) {
            logger.error("Ignore this exception", e);
        } finally {
            try {
                if (rs != null){
                    rs.close();
                }
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
            try {
                if (countStmt != null){
                    countStmt.close();
                }
            } catch (SQLException e) {
                logger.error("Ignore this exception", e);
            }
        }

    }

    /**
     * 對SQL參數(shù)(?)設(shè)值
     *
     * @param ps
     * @param mappedStatement
     * @param boundSql
     * @param parameterObject
     * @throws SQLException
     */
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
                               Object parameterObject) throws SQLException {
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(ps);
    }

    /**
     * 根據(jù)數(shù)據(jù)庫類型柴罐,生成特定的分頁sql
     *
     * @param sql
     * @param page
     * @return
     */
    private String buildPageSql(String sql, PageParameter page) {
        if (page != null) {
            StringBuilder pageSql = new StringBuilder();
            pageSql = buildPageSqlForMysql(sql,page);
            return pageSql.toString();
        } else {
            return sql;
        }
    }

    /**
     * mysql的分頁語句
     *
     * @param sql
     * @param page
     * @return String
     */
    public StringBuilder buildPageSqlForMysql(String sql, PageParameter page) {
        StringBuilder pageSql = new StringBuilder(100);
        String beginrow = String.valueOf((page.getCurrentPage() - 1) * page.getPageSize());
        pageSql.append(sql);
        pageSql.append(" limit " + beginrow + "," + page.getPageSize());
        return pageSql;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
    }

}
這里碰到一個(gè)比較有趣的問題,就是sql如果是foreach參數(shù)综芥,在攔截后無法注入丽蝎。需要加入以下代碼才可以(有得資料上只提到重置metaParameters)。
Field metaParamsField = ReflectUtil.getFieldByFieldName(boundSql, "metaParameters");
if (metaParamsField != null) {
    try {
        MetaObject mo = (MetaObject) ReflectUtil.getValueByFieldName(boundSql, "metaParameters");
        ReflectUtil.setValueByFieldName(countBS, "metaParameters", mo);
    } catch (SecurityException | NoSuchFieldException | IllegalArgumentException
            | IllegalAccessException e) {
        // TODO Auto-generated catch block
         logger.error("Ignore this exception", e);
    }
}
Field additionalField = ReflectUtil.getFieldByFieldName(boundSql, "additionalParameters");
if (additionalField != null) {
    try {
        Map<String, Object> map = (Map<String, Object>) ReflectUtil.getValueByFieldName(boundSql, "additionalParameters");
        ReflectUtil.setValueByFieldName(countBS, "additionalParameters", map);
    } catch (SecurityException | NoSuchFieldException | IllegalArgumentException
            | IllegalAccessException e) {
        // TODO Auto-generated catch block
        logger.error("Ignore this exception", e);
    }
}
讀寫分離倒是寫好了膀藐,但是發(fā)現(xiàn)增加了mysql一主多從的讀寫分離后屠阻,此分頁攔截器直接失效。
最后分析原因是因?yàn)槎罡鳎覀冊谧鲋鲝姆蛛x時(shí)国觉,自定義了SqlSessionFactory,導(dǎo)致此攔截器沒有注入虾啦。
在上面第4步中麻诀,DataSourceSqlSessionFactory中注入攔截器即可,具體代碼如下
通過注解引入攔截器類:
@Import({DataSourceConfiguration.class,PageInterceptor.class})
注入攔截器
@Autowired
    private PageInterceptor pageInterceptor;
SqlSessionFactoryBean中設(shè)置攔截器
sqlSessionFactoryBean.setPlugins(newInterceptor[]{pageInterceptor});
這里碰到一個(gè)坑傲醉,就是設(shè)置plugins時(shí)必須在sqlSessionFactoryBean.getObject()之前蝇闭。
SqlSessionFactory在生成的時(shí)候就會獲取plugins,并設(shè)置到Configuration中硬毕,如果在之后設(shè)置則不會注入呻引。
可跟蹤源碼看到:
sqlSessionFactoryBean.getObject()
public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }

    return this.sqlSessionFactory;
}
public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
              "Property 'configuration' and 'configLocation' can not specified with together");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }
buildSqlSessionFactory()
if (!isEmpty(this.plugins)) {
      for (Interceptor plugin : this.plugins) {
        configuration.addInterceptor(plugin);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registered plugin: '" + plugin + "'");
        }
      }
    }
最后貼上正確的配置代碼(DataSourceSqlSessionFactory代碼片段)
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory() throws Exception {
        logger.info("======================= init sqlSessionFactory");
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setPlugins(new Interceptor[]{pageInterceptor});
        sqlSessionFactoryBean.setDataSource(roundRobinDataSourceProxy());
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocations));
        sqlSessionFactoryBean.setTypeAliasesPackage(aliasesPackage);
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
        sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);

        return sqlSessionFactory;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吐咳,隨后出現(xiàn)的幾起案子逻悠,更是在濱河造成了極大的恐慌元践,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件童谒,死亡現(xiàn)場離奇詭異单旁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)饥伊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門象浑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人撵渡,你說我怎么就攤上這事融柬。” “怎么了趋距?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長越除。 經(jīng)常有香客問我节腐,道長,這世上最難降的妖魔是什么摘盆? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任翼雀,我火速辦了婚禮,結(jié)果婚禮上孩擂,老公的妹妹穿的比我還像新娘狼渊。我一直安慰自己,他們只是感情好类垦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布狈邑。 她就那樣靜靜地躺著,像睡著了一般蚤认。 火紅的嫁衣襯著肌膚如雪米苹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天砰琢,我揣著相機(jī)與錄音蘸嘶,去河邊找鬼。 笑死陪汽,一個(gè)胖子當(dāng)著我的面吹牛训唱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挚冤,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼况增,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了你辣?” 一聲冷哼從身側(cè)響起巡通,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤尘执,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后宴凉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體誊锭,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年弥锄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丧靡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡籽暇,死狀恐怖温治,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情戒悠,我是刑警寧澤熬荆,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站绸狐,受9級特大地震影響卤恳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寒矿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一突琳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧符相,春花似錦拆融、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至孕索,卻和暖如春逛艰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背搞旭。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工散怖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肄渗。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓镇眷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親翎嫡。 傳聞我的和親對象是個(gè)殘疾皇子欠动,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355