SpringBoot動(dòng)態(tài)數(shù)據(jù)源配置

主要的思路:配置多個(gè)數(shù)據(jù)源加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象中,根據(jù)實(shí)際的情況動(dòng)態(tài)的切換到相應(yīng)的數(shù)據(jù)源东涡。


架構(gòu)流程圖:



執(zhí)行的步驟:建立數(shù)據(jù)源->數(shù)據(jù)源加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象->動(dòng)態(tài)數(shù)據(jù)源的配置->動(dòng)態(tài)切換


1、建立數(shù)據(jù)源

這一步比較簡(jiǎn)單折柠,根據(jù)連接池(例如:HikariCP蚣抗、c3p0)建立相關(guān)的數(shù)據(jù)源道偷,本例中使用HikariCP玖雁。

@Configuration// 配置數(shù)據(jù)源
@RefreshScope
public class DataSourceConfigure implements Ordered, EnvironmentAware {

    private final Logger logger = LoggerFactory.getLogger(DataSourceConfigure.class);

    private static final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";

    private Environment environment;

    private Class dataSourceClass;

    public DataSourceConfigure() {
        Class dataSourceClass = null;
        try {
            dataSourceClass = Class.forName(DATASOURCE_TYPE_DEFAULT);
        } catch (ClassNotFoundException e) {
            logger.error("不存在類:" + DATASOURCE_TYPE_DEFAULT);
            e.printStackTrace();
        }
        this.dataSourceClass = dataSourceClass;
    }

    @Bean
    @RefreshScope
    @Primary
    public DataSource xxx() {
        logger.info("注冊(cè)數(shù)據(jù)源:xxx");
        return (DataSource) Binder.get(environment).bind( "custom.datasource.xxx", this.dataSourceClass).orElse(null);
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

如上面的代碼段所示更扁,根據(jù)自己的實(shí)際內(nèi)容建立相應(yīng)的數(shù)據(jù)源。


2赫冬、數(shù)據(jù)源加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象

這一步把相關(guān)的數(shù)據(jù)源增加到動(dòng)態(tài)數(shù)據(jù)源對(duì)象中浓镜,直接上代碼。

@Configuration
@EnableAutoConfiguration
public class DatabaseConfiguration {
    @Autowired
    private ApplicationContext appContext;

    private static final String DATASOURCE_PREFIX = "custom.datasource";

    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        RoutingDataSource proxy = new RoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap();
        //獲取到所有數(shù)據(jù)源的名稱.
        Map<String, Map<String, Object>> map = Binder.get(appContext.getEnvironment()).bind(DATASOURCE_PREFIX, Map.class).orElse(null);
        /**
         * 獲取每個(gè)數(shù)據(jù)源的屬性
         */
        map.forEach((key, value) -> {
            DataSource dataSource = (DataSource) appContext.getBean(key);
            targetDataSources.put(key, dataSource);
            if (value.get("isPrimary") != null && value.get("isPrimary").equals(1)) {
                proxy.setDefaultTargetDataSource(dataSource);
            }
        });
        proxy.setTargetDataSources(targetDataSources);
        return proxy;

    }

}

3劲厌、動(dòng)態(tài)數(shù)據(jù)源的配置(根據(jù)實(shí)際的使用膛薛,配置相關(guān)的配置,本例中使用的是Spring-data-jpa补鼻。)

下面的代碼只是簡(jiǎn)單的配置一下JPA配置類哄啄,使得支持jpa的使用,需要根據(jù)實(shí)際情況修改辽幌。

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactoryDynamic",
        transactionManagerRef = "transactionManagerDynamic",
        basePackages = {"com.xxx.gbimclientdynamic.repository"},//設(shè)置Repository所在位置
        repositoryFactoryBeanClass = BaseRepositoryFactoryBean.class
)
public class DynamicConfig {

    @Autowired
    @Qualifier("routingDataSource") //配置中定義的名字
    private DataSource routingDataSource;

    @Bean(name = "entityManagerFactoryDynamic")
    @Primary
    public EntityManagerFactory entityManagerFactoryDynamic() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.xxx.gbimclientdynamic.entity");
        factory.setDataSource(routingDataSource);//數(shù)據(jù)源
        factory.setPersistenceUnitName("dynamicPersistenceUnit");
        Properties properties = new Properties();
        properties.put("hibernate.show_sql", true);
        properties.put("hibernate.dialect","org.hibernate.dialect.SQLServer2012Dialect");
        factory.setJpaProperties(properties);
        factory.afterPropertiesSet();//在完成了其它所有相關(guān)的配置加載以及屬性設(shè)置后,才初始化
        return factory.getObject();
    }

    @Bean(name = "transactionManagerDynamic")
    @Primary
    PlatformTransactionManager transactionManagerDynamic() {
        return new JpaTransactionManager(entityManagerFactoryDynamic());
    }
}

4增淹、動(dòng)態(tài)切換

這一步的這個(gè)例子的重點(diǎn)椿访,這里會(huì)說得稍微詳細(xì)一些乌企。
首先要考慮的就是怎么可以實(shí)現(xiàn)切換呢?Spring提供了AbstractRoutingDataSource來實(shí)現(xiàn)這樣的功能成玫,AbstractRoutingDataSource的功能是在其中可以根據(jù)key值動(dòng)態(tài)切換到具體的數(shù)據(jù)源加酵。
AbstractRoutingDataSource的具體的實(shí)現(xiàn)拳喻,可以看一下具體的實(shí)現(xiàn)代碼。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  ......
  ......
}

由代碼中看到AbstractRoutingDataSource 是繼承于 AbstractDataSource 猪腕,而AbstractDataSource是DataSource 的一個(gè)實(shí)現(xiàn)類冗澈,所以這里主要是要看一下獲得連接的方法,即如下:

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

getConnection()方法中的determineTargetDataSource()明顯就是確定數(shù)據(jù)源的方法陋葡,所以我們繼續(xù)看一下determineTargetDataSource()這個(gè)方法亚亲。

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

determineTargetDataSource()中,determineCurrentLookupKey();是獲取數(shù)據(jù)源dataSource的key值的腐缤,在AbstractRoutingDataSource類中捌归,determineCurrentLookupKey()是一個(gè)抽象方法,就該子類就必須重新該方法岭粤。接著根據(jù)determineCurrentLookupKey()獲得的key值惜索,在this.resolvedDataSources中獲得dataSource。如果不存在剃浇,就根據(jù)默認(rèn)設(shè)置默認(rèn)的數(shù)據(jù)源巾兆。
所以,根據(jù)源碼虎囚,繼承AbstractRoutingDataSource類角塑,并重寫其中的determineCurrentLookupKey()方法,就可以實(shí)現(xiàn)數(shù)據(jù)源的切換溜宽。

上述內(nèi)容基本說明了如何切的問題吉拳,接下來需要考慮在何時(shí)切,并且應(yīng)該如何實(shí)現(xiàn)的問題适揉。
其實(shí)這里對(duì)簡(jiǎn)單的就是利用AOP了留攒,AOP的具體內(nèi)容可以自己去網(wǎng)上找一下,這里就不作詳細(xì)的介紹了嫉嘀。就是利用AOP在調(diào)用需要調(diào)用數(shù)據(jù)庫的方法前就設(shè)定好需要的數(shù)據(jù)源(利用上述的determineCurrentLookupKey())炼邀。

所以,動(dòng)態(tài)切換的大體思路是這樣的:
利用AOP剪侮,調(diào)用需要調(diào)用數(shù)據(jù)庫的方法前利用determineCurrentLookupKey()設(shè)定好需要的數(shù)據(jù)源拭宁。

完整代碼如下:

//動(dòng)態(tài)數(shù)據(jù)源存放對(duì)象,保存key值瓣俯,用于動(dòng)態(tài)切換使用
public class DbContextHolder {

    //線程本地環(huán)境杰标,ThreadLocal 是用于線性安全的
    private static final ThreadLocal<String> dataSources = new ThreadLocal();


    //設(shè)置數(shù)據(jù)源
    public static void setDataSource(String customerType) {
        dataSources.set(customerType);
    }

    //獲取數(shù)據(jù)源
    public static String getDataSource() {
        return dataSources.get();
    }

    //清除數(shù)據(jù)源
    public static void clearDataSource() {
        dataSources.remove();
    }

    
}

//用于動(dòng)態(tài)切換,就是上面說的源碼的這部分
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder .getDataSource();
    }

}


//定義一個(gè)注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DbName {
    String value() default "";
}

//切面
@Aspect
@Component
public class DataSourceAspect {

    private Logger logger = LoggerFactory.getLogger(getClass().getName());


    //作用于service層 ||@args(com.xxx.connection.aop.RoutingDbName)
    //這個(gè)匹配能匹配到類彩匕,方法腔剂,無法匹配到參數(shù)
    @Pointcut("execution(public * com.xxx..*.service.impl..*.*(..))&&(@target(com.xxx.connection.aop.DbName)||@annotation(com.xxx.connection.aop.DbName))||execution(public * com.xxx..*.service.impl..*.*(.., @DbName(*), ..))")
    public void routingDsPointCut() {
    }


    //方法切面,不指定注解的值時(shí)驼仪,第一個(gè)參數(shù)作為dbName掸犬,指定注解的值袜漩,可指定數(shù)據(jù)源,優(yōu)先級(jí):類->方法->參數(shù)
    @Around("routingDsPointCut()")
    public Object proceeRouting(ProceedingJoinPoint joinpoint) throws Throwable {
        logger.info("set connection on service");
        Object result = null;
        MethodSignature signature = (MethodSignature) joinpoint.getSignature();
        Object[] args = joinpoint.getArgs();
        DbName dbName = null;
        String dbStr = "";
        //獲取切面方法
        Method method = signature.getMethod();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Class[] parameterTypes = method.getParameterTypes();
        //獲取參數(shù)注解
        for(int i =0;i<parameterAnnotations.length;i++ ){
            Annotation[] annotations = parameterAnnotations[i];
            Class parameterType = parameterTypes[i];
            for(int j=0;j<annotations.length;j++){
                Annotation annotation = annotations[j];
                if(annotation instanceof DbName && "String".equals(parameterType.getSimpleName())){
                    dbName = (DbName) annotation;
                    dbStr = (String) args[j];
                    logger.info("取得參數(shù)的注解湾碎!");
                }
            }
        }
        //獲取方法注解
        if(dbName==null){
            dbName = method.getAnnotation(DbName.class);
            if(dbName!=null){
                logger.info("取得方法的注解宙攻!");
            }
        }
        //獲取類注解
        if(dbName==null){
            String targetClass = joinpoint.getTarget().getClass().getName();
            //獲取切面所在類
            Class clazz = Class.forName(targetClass);
            dbName =(DbName) clazz.getAnnotation(DbName.class);
            if(dbName!=null){
                logger.info("取得類的注解!");
            }
        }
        //切換數(shù)據(jù)源
        if(dbName!=null){
         
            if(dbName.value() != null&&!"".equals(dbName.value())){ //指定數(shù)據(jù)庫時(shí)
                DbContextHolder.setDataSource(dbName.value());
                logger.info("使用注解的值介褥,當(dāng)前使用"+dbName.value()+"數(shù)據(jù)源座掘!");
            }else if(!"".equals(dbStr)){
                DbContextHolder.setDataSource(dbStr);
                logger.info("使用參數(shù)的值,當(dāng)前使用"+dbStr+"數(shù)據(jù)源柔滔!");
            }else if(args.length>0){
                DbContextHolder.setDataSource((String)args[0]);
                logger.info("使用第一個(gè)參數(shù)的值雹顺,當(dāng)前使用"+(String)args[0]+"數(shù)據(jù)源!");
            }else { //沒有指定數(shù)據(jù)源
                logger.info("注解的值不能為空廊遍!使用默認(rèn)數(shù)據(jù)源嬉愧!");
            }
        }else{
            logger.info("service不切換數(shù)據(jù)源!");
        }
        result = joinpoint.proceed();//執(zhí)行前后
        logger.info("數(shù)據(jù)源切換完畢喉前!");
        return result;
    }


   
}



5没酣、拓展

上述的四步已經(jīng)基本的說明了一個(gè)簡(jiǎn)單的動(dòng)態(tài)數(shù)據(jù)源切換了,但是上面提交的數(shù)據(jù)源是已經(jīng)確定了卵迂,如果數(shù)據(jù)源數(shù)據(jù)源還不確定的情況下裕便,能不能動(dòng)態(tài)的生成數(shù)據(jù)源動(dòng)態(tài)的切換呢?答案是肯定的见咒。這就是本部分要說的內(nèi)容偿衰。
其實(shí)實(shí)現(xiàn)的思路也很簡(jiǎn)單:在切換前看看有沒有需要切換的數(shù)據(jù)源,如果沒有改览,就根據(jù)相關(guān)的配置生成下翎。所以,內(nèi)容基本和上述的四步很類似宝当,唯一不一樣的就是繼承AbstractRoutingDataSource類重新的determineCurrentLookupKey()方法视事。直接上代碼:

public class RoutingDataSource extends AbstractRoutingDataSource {
    /**
     * 默認(rèn)值數(shù)據(jù)源類型,如需要?jiǎng)e的數(shù)據(jù)源需要修改
     */
    private final String DATASOURCE_TYPE_DEFAULT = "com.zaxxer.hikari.HikariDataSource";

    /**
     * 別名
     */
    private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
    private Logger logger = LoggerFactory.getLogger(getClass().getName());
    @Override
    protected Object determineCurrentLookupKey() {

        //需要切換的數(shù)據(jù)庫
        String dbName = DbContextHolder.getDataSource();
        if (dbName == null) {
            return null;
        }
        dbName=dbName.toLowerCase();
        try {
            /**
             * 獲取AbstractRoutingDataSource的targetDataSources屬性,該屬性存放數(shù)據(jù)源屬性
             *`
             **/
            Map<Object, Object> targetSourceMap = getTargetSource();
            synchronized (this) {
                //判斷targetDataSources中是否已經(jīng)存在要設(shè)置的數(shù)據(jù)源bean
                // 存在的話,則直接返回beanName
                // 不存在的話庆揩,則需要建立數(shù)據(jù)源

                if (!targetSourceMap.keySet().contains(dbName)) {
                    logger.info("數(shù)據(jù)不存在俐东,建立數(shù)據(jù)源。");
                    //建立數(shù)據(jù)源
                    Object dataSource = createDataSource(dbName,EnvironmentUtil.getEnvironment());
                    logger.info("建立數(shù)據(jù)源成功订晌。");
                    /**
                     * 在創(chuàng)建后的bean,放入到targetDataSources Map中
                     * **/
                    targetSourceMap.put(dbName, dataSource);
                    logger.info("數(shù)據(jù)源放入到targetDataSources Map中虏辫。");
                    //通知spring有bean更新
                    super.afterPropertiesSet();
                    logger.info("通知spring有bean更新");
                }else{
                    logger.info("數(shù)據(jù)已存在,切換到相應(yīng)的數(shù)據(jù)源");
                }

            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return DbContextHolder.getDataSource().toLowerCase();
    }

    //獲取AbstractRoutingDataSource的targetDataSources屬性,該屬性存放數(shù)據(jù)源屬性
    @SuppressWarnings("unchecked")
    public Map<Object, Object> getTargetSource() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
        Field field = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");
        field.setAccessible(true);
        return (Map<Object, Object>) field.get(this);
    }

    /**
     * 根據(jù)數(shù)據(jù)源信息在spring中創(chuàng)建bean,并返回
     * @param dbName 數(shù)據(jù)源信息
     * @return 數(shù)據(jù)源
     * @throws IllegalAccessException
     */
    public Object createDataSource(String dbName,Environment environment) throws Exception {
        Class clazz = Class.forName(DATASOURCE_TYPE_DEFAULT);
        //獲取參數(shù)
        Map config=getBeanDef(environment,dbName);
        // 通過類型綁定參數(shù)并獲得實(shí)例對(duì)象锈拨,若數(shù)據(jù)源修改時(shí)砌庄,這個(gè)需要修改
        DataSource consumerDatasource = bind(clazz, config);
        return consumerDatasource;
    }

    //獲取參數(shù)
    private Map<String, Object> getBeanDef(Environment environment,String dbName) {
        Map<String, Object> dataSourceMap = (Map)Binder.get(environment).bind("custom.datasource", Map.class).orElse(null);
        if(dataSourceMap != null && !dataSourceMap.isEmpty()) {
            Iterator var4 = dataSourceMap.entrySet().iterator();
            if(var4.hasNext()){
                Map.Entry<String, Map<String, Object>> entry = (Map.Entry)var4.next();
                String jdbcurl=null!=entry.getValue().get("jdbcurl")?entry.getValue().get("jdbcurl").toString():"";
                jdbcurl=jdbcurl.toLowerCase().replace(entry.getKey().toLowerCase(),dbName);
                entry.getValue().put("jdbcurl",jdbcurl);
                return  entry.getValue();

            }
        }

        return dataSourceMap;
    }
    //綁定數(shù)據(jù)源
    private <T extends DataSource> T bind(Class<T> clazz, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
        // 通過類型綁定參數(shù)并獲得實(shí)例對(duì)象
        return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
    }

}

在這里determineCurrentLookupKey()方法與第四步不相同的地方主要是這部分:

   /**
             * 獲取AbstractRoutingDataSource的targetDataSources屬性,該屬性存放數(shù)據(jù)源屬性
             *`
             **/
            Map<Object, Object> targetSourceMap = getTargetSource();
            synchronized (this) {
                //判斷targetDataSources中是否已經(jīng)存在要設(shè)置的數(shù)據(jù)源bean
                // 存在的話,則直接返回beanName
                // 不存在的話,則需要建立數(shù)據(jù)源

                if (!targetSourceMap.keySet().contains(dbName)) {
                    logger.info("數(shù)據(jù)不存在,建立數(shù)據(jù)源鹤耍。");
                    //建立數(shù)據(jù)源
                    Object dataSource = createDataSource(dbName,EnvironmentUtil.getEnvironment());
                    logger.info("建立數(shù)據(jù)源成功。");
                    /**
                     * 在創(chuàng)建后的bean,放入到targetDataSources Map中
                     * **/
                    targetSourceMap.put(dbName, dataSource);
                    logger.info("數(shù)據(jù)源放入到targetDataSources Map中验辞。");
                    //通知spring有bean更新
                    super.afterPropertiesSet();
                    logger.info("通知spring有bean更新");
                }else{
                    logger.info("數(shù)據(jù)已存在稿黄,切換到相應(yīng)的數(shù)據(jù)源");
                }

簡(jiǎn)單的看一下,這部分其實(shí)是和剛剛提到的思路完全一致的跌造。首先判斷一下有沒有需要的數(shù)據(jù)源(if (!targetSourceMap.keySet().contains(dbName)))杆怕,如果沒有的情況建立數(shù)據(jù)源(createDataSource(dbName,EnvironmentUtil.getEnvironment())),然后加到AbstractRoutingDataSourcetargetDataSources屬性中(targetSourceMap.put(dbName, dataSource);)壳贪,最后通知spring有bean更新(super.afterPropertiesSet();)陵珍。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市违施,隨后出現(xiàn)的幾起案子互纯,更是在濱河造成了極大的恐慌,老刑警劉巖磕蒲,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件留潦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡辣往,警方通過查閱死者的電腦和手機(jī)兔院,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來站削,“玉大人坊萝,你說我怎么就攤上這事⌒砥穑” “怎么了十偶?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)园细。 經(jīng)常有香客問我扯键,道長(zhǎng),這世上最難降的妖魔是什么珊肃? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任荣刑,我火速辦了婚禮,結(jié)果婚禮上伦乔,老公的妹妹穿的比我還像新娘厉亏。我一直安慰自己围肥,他們只是感情好拦焚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布侨拦。 她就那樣靜靜地躺著横朋,像睡著了一般芥玉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笋婿,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天吏够,我揣著相機(jī)與錄音,去河邊找鬼训柴。 笑死哑舒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幻馁。 我是一名探鬼主播洗鸵,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼仗嗦!你這毒婦竟也來了膘滨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤稀拐,失蹤者是張志新(化名)和其女友劉穎火邓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體德撬,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贡翘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砰逻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸣驱。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蝠咆,靈堂內(nèi)的尸體忽然破棺而出踊东,到底是詐尸還是另有隱情,我是刑警寧澤刚操,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布闸翅,位于F島的核電站,受9級(jí)特大地震影響菊霜,放射性物質(zhì)發(fā)生泄漏坚冀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一鉴逞、第九天 我趴在偏房一處隱蔽的房頂上張望记某。 院中可真熱鬧,春花似錦构捡、人聲如沸液南。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽滑凉。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間畅姊,已是汗流浹背咒钟。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留若未,地道東北人朱嘴。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像陨瘩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子级乍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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