Mybatis 分頁詳解

前言

在學(xué)習(xí)mybatis等持久層框架的時候羹幸,會經(jīng)常對數(shù)據(jù)進(jìn)行增刪改查操作脊髓,使用最多的是對數(shù)據(jù)庫進(jìn)行查詢操作,如果查詢大量數(shù)據(jù)的時候睹欲,我們往往使用分頁進(jìn)行查詢供炼,也就是每次處理小部分?jǐn)?shù)據(jù),這樣對數(shù)據(jù)庫壓力就在可控范圍內(nèi)窘疮。

分頁的幾種方式

1. 內(nèi)存分頁

內(nèi)存分頁的原理比較sb袋哼,就是一次性查詢數(shù)據(jù)庫中所有滿足條件的記錄,將這些數(shù)據(jù)臨時保存在集合中闸衫,再通過List的subList方法涛贯,獲取到滿足條件的記錄,由于太sb蔚出,直接忽略該種方式的分頁弟翘。

2. 物理分頁

在了解到通過內(nèi)存分頁的缺陷后,我們發(fā)現(xiàn)不能每次都對數(shù)據(jù)庫中的所有數(shù)據(jù)都檢索骄酗。然后在程序中對獲取到的大量數(shù)據(jù)進(jìn)行二次操作稀余,這樣對空間和性能都是極大的損耗。所以我們希望能直接在數(shù)據(jù)庫語言中只檢索符合條件的記錄趋翻,不需要在通過程序?qū)ζ渥魈幚砭α铡_@時,物理分頁技術(shù)橫空出世。

物理分頁是借助sql語句進(jìn)行分頁师骗,比如mysql是通過limit關(guān)鍵字历等,oracle是通過rownum等;其中mysql的分頁語法如下:

select * from table limit 0,30

MyBatis 分頁

1.借助sql進(jìn)行分頁

通過sql語句進(jìn)行分頁的實現(xiàn)很簡單,我們先在StudentMapper接口中添加sql語句的查詢方法辟癌,如下:

List queryStudentsBySql(@Param("offset") int offset, @Param("limit") int limit);

StudentMapper.xml 配置如下:

select * from student limit #{offset} , #{limit}

客戶端使用的時候如下:

public List queryStudentsBySql(int offset, int pageSize) {

return studentMapper.queryStudentsBySql(offset,pageSize);

}

sql分頁語句如下:select * from table limit index, pageSize;

缺點:雖然這里實現(xiàn)了按需查找寒屯,每次檢索得到的是指定的數(shù)據(jù)。但是每次在分頁的時候都需要去編寫limit語句黍少,很冗余, 其次另外如果想知道總條數(shù)寡夹,還需要另外寫sql去統(tǒng)計查詢。而且不方便統(tǒng)一管理厂置,維護(hù)性較差要出。所以我們希望能夠有一種更方便的分頁實現(xiàn)。

2. 攔截器分頁

攔截器的一個作用就是我們可以攔截某些方法的調(diào)用农渊,我們可以選擇在這些被攔截的方法執(zhí)行前后加上某些邏輯,也可以在執(zhí)行這些被攔截的方法時執(zhí)行自己的邏輯而不再執(zhí)行被攔截的方法或颊。Mybatis攔截器設(shè)計的一個初衷就是為了供用戶在某些時候可以實現(xiàn)自己的邏輯而不必去動Mybatis固有的邏輯砸紊。打個比方,對于Executor囱挑,Mybatis中有幾種實現(xiàn):BatchExecutor醉顽、ReuseExecutor、SimpleExecutor和CachingExecutor平挑。這個時候如果你覺得這幾種實現(xiàn)對于Executor接口的query方法都不能滿足你的要求游添,那怎么辦呢?是要去改源碼嗎通熄?當(dāng)然不唆涝。我們可以建立一個Mybatis攔截器用于攔截Executor接口的query方法,在攔截之后實現(xiàn)自己的query方法邏輯唇辨,之后可以選擇是否繼續(xù)執(zhí)行原來的query方法廊酣。

Interceptor接口

對于攔截器Mybatis為我們提供了一個Interceptor接口,通過實現(xiàn)該接口就可以定義我們自己的攔截器赏枚。我們先來看一下這個接口的定義:

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);

}

我們可以看到在該接口中一共定義有三個方法亡驰,intercept、plugin和setProperties饿幅。plugin方法是攔截器用于封裝目標(biāo)對象的凡辱,通過該方法我們可以返回目標(biāo)對象本身,也可以返回一個它的代理栗恩。當(dāng)返回的是代理的時候我們可以對其中的方法進(jìn)行攔截來調(diào)用intercept方法透乾,當(dāng)然也可以調(diào)用其他方法,這點將在后文講解。setProperties方法是用于在Mybatis配置文件中指定一些屬性的续徽。

定義自己的Interceptor最重要的是要實現(xiàn)plugin方法和intercept方法蚓曼,在plugin方法中我們可以決定是否要進(jìn)行攔截進(jìn)而決定要返回一個什么樣的目標(biāo)對象。而intercept方法就是要進(jìn)行攔截的時候要執(zhí)行的方法钦扭。

對于plugin方法而言纫版,其實Mybatis已經(jīng)為我們提供了一個實現(xiàn)。Mybatis中有一個叫做Plugin的類客情,里面有一個靜態(tài)方法wrap(Object target,Interceptor interceptor)其弊,通過該方法可以決定要返回的對象是目標(biāo)對象還是對應(yīng)的代理。這里我們先來看一下Plugin的源碼:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;

import java.util.HashMap;

import java.util.HashSet;

import java.util.Map;

import java.util.Set;

import org.apache.ibatis.reflection.ExceptionUtil;

public class Plugin implements InvocationHandler {

private Object target;

private Interceptor interceptor;

private Map, Set> signatureMap;

private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) {

this.target = target;

this.interceptor = interceptor;

this.signatureMap = signatureMap;

}

public static Object wrap(Object target, Interceptor interceptor) {

Map, Set> signatureMap = getSignatureMap(interceptor);

Class type = target.getClass();

Class[] interfaces = getAllInterfaces(type, signatureMap);

if (interfaces.length > 0) {

return Proxy.newProxyInstance(

type.getClassLoader(),

interfaces,

new Plugin(target, interceptor, signatureMap));

}

return target;

}

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

try {

Set methods = signatureMap.get(method.getDeclaringClass());

if (methods != null && methods.contains(method)) {

return interceptor.intercept(new Invocation(target, method, args));

}

return method.invoke(target, args);

} catch (Exception e) {

throw ExceptionUtil.unwrapThrowable(e);

}

}

private static Map, Set> getSignatureMap(Interceptor interceptor) {

Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);

if (interceptsAnnotation == null) { // issue #251

throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());

}

Signature[] sigs = interceptsAnnotation.value();

Map, Set> signatureMap = new HashMap, Set>();

for (Signature sig : sigs) {

Set methods = signatureMap.get(sig.type());

if (methods == null) {

methods = new HashSet();

signatureMap.put(sig.type(), methods);

}

try {

Method method = sig.type().getMethod(sig.method(), sig.args());

methods.add(method);

} catch (NoSuchMethodException e) {

throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);

}

}

return signatureMap;

}

private static Class[] getAllInterfaces(Class type, Map, Set> signatureMap) {

Set> interfaces = new HashSet>();

while (type != null) {

for (Class c : type.getInterfaces()) {

if (signatureMap.containsKey(c)) {

interfaces.add(c);

}

}

type = type.getSuperclass();

}

return interfaces.toArray(new Class[interfaces.size()]);

}

}

我們先看一下Plugin的wrap方法膀斋,它根據(jù)當(dāng)前的Interceptor上面的注解定義哪些接口需要攔截梭伐,然后判斷當(dāng)前目標(biāo)對象是否有實現(xiàn)對應(yīng)需要攔截的接口,如果沒有則返回目標(biāo)對象本身仰担,如果有則返回一個代理對象糊识。而這個代理對象的InvocationHandler正是一個Plugin。所以當(dāng)目標(biāo)對象在執(zhí)行接口方法時摔蓝,如果是通過代理對象執(zhí)行的赂苗,則會調(diào)用對應(yīng)InvocationHandler的invoke方法,也就是Plugin的invoke方法贮尉。所以接著我們來看一下該invoke方法的內(nèi)容拌滋。這里invoke方法的邏輯是:如果當(dāng)前執(zhí)行的方法是定義好的需要攔截的方法,則把目標(biāo)對象猜谚、要執(zhí)行的方法以及方法參數(shù)封裝成一個Invocation對象败砂,再把封裝好的Invocation作為參數(shù)傳遞給當(dāng)前攔截器的intercept方法。如果不需要攔截魏铅,則直接調(diào)用當(dāng)前的方法昌犹。Invocation中定義了定義了一個proceed方法,其邏輯就是調(diào)用當(dāng)前方法览芳,所以如果在intercept中需要繼續(xù)調(diào)用當(dāng)前方法的話可以調(diào)用invocation的procced方法祭隔。

這就是Mybatis中實現(xiàn)Interceptor攔截的一個思想,如果用戶覺得這個思想有問題或者不能完全滿足你的要求的話可以通過實現(xiàn)自己的Plugin來決定什么時候需要代理什么時候需要攔截路操。以下講解的內(nèi)容都是基于Mybatis的默認(rèn)實現(xiàn)即通過Plugin來管理Interceptor來講解的疾渴。

對于實現(xiàn)自己的Interceptor而言有兩個很重要的注解,一個是@Intercepts屯仗,其值是一個@Signature數(shù)組搞坝。@Intercepts用于表明當(dāng)前的對象是一個Interceptor,而@Signature則表明要攔截的接口魁袜、方法以及對應(yīng)的參數(shù)類型桩撮。

首先我們看一下攔截器的具體實現(xiàn)敦第,在這里我們需要攔截所有以PageDto作為入?yún)⒌乃胁樵冋Z句,自動以攔截器需要繼承Interceptor類店量,PageDto代碼如下:

?

import java.util.Date;

import java.util.List;

?

/**

* Created by chending on 16/3/27.

*/

public class PageDto {

?

private Integer rows = 10;

?

private Integer offset = 0;

?

private Integer pageNo = 1;

?

private Integer totalRecord = 0;

?

private Integer totalPage = 1;

?

private Boolean hasPrevious = false;

?

private Boolean hasNext = false;

?

private Date start;

?

private Date end;

?

private T searchCondition;

?

private List dtos;

?

?

public Date getStart() {

return start;

}

?

public void setStart(Date start) {

this.start = start;

}

?

public Date getEnd() {

return end;

}

?

public void setEnd(Date end) {

this.end = end;

}

?

public void setDtos(List dtos){

this.dtos = dtos;

}

?

public List getDtos(){

return dtos;

}

?

public Integer getRows() {

return rows;

}

?

public void setRows(Integer rows) {

this.rows = rows;

}

?

public Integer getOffset() {

return offset;

}

?

public void setOffset(Integer offset) {

this.offset = offset;

}

?

public Integer getPageNo() {

return pageNo;

}

?

public void setPageNo(Integer pageNo) {

this.pageNo = pageNo;

}

?

public Integer getTotalRecord() {

return totalRecord;

}

?

public void setTotalRecord(Integer totalRecord) {

this.totalRecord = totalRecord;

}

?

?

public T getSearchCondition() {

return searchCondition;

}

?

public void setSearchCondition(T searchCondition) {

this.searchCondition = searchCondition;

}

?

public Integer getTotalPage() {

return totalPage;

}

?

public void setTotalPage(Integer totalPage) {

this.totalPage = totalPage;

}

?

public Boolean getHasPrevious() {

return hasPrevious;

}

?

public void setHasPrevious(Boolean hasPrevious) {

this.hasPrevious = hasPrevious;

}

?

public Boolean getHasNext() {

return hasNext;

}

?

public void setHasNext(Boolean hasNext) {

this.hasNext = hasNext;

}

}

?

自定義攔截器PageInterceptor 代碼如下:

?

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.util.List;

import java.util.Properties;

import me.ele.elog.Log;

import me.ele.elog.LogFactory;

import me.ele.gaos.common.util.CommonUtil;

import org.apache.ibatis.executor.parameter.ParameterHandler;

import org.apache.ibatis.executor.statement.RoutingStatementHandler;

import org.apache.ibatis.executor.statement.StatementHandler;

import org.apache.ibatis.mapping.BoundSql;

import org.apache.ibatis.mapping.MappedStatement;

import org.apache.ibatis.mapping.ParameterMapping;

import org.apache.ibatis.plugin.Interceptor;

import org.apache.ibatis.plugin.Intercepts;

import org.apache.ibatis.plugin.Invocation;

import org.apache.ibatis.plugin.Plugin;

import org.apache.ibatis.plugin.Signature;

import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;

?

?

/**

*

* 分頁攔截器芜果,用于攔截需要進(jìn)行分頁查詢的操作,然后對其進(jìn)行分頁處理融师。

*

*/

@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class,Integer.class})})

public class PageInterceptor implements Interceptor {

private String dialect = ""; //數(shù)據(jù)庫方言

?

private Log log = LogFactory.getLog(PageInterceptor.class);

?

@Override

public Object intercept(Invocation invocation) throws Throwable {

if(invocation.getTarget() instanceof RoutingStatementHandler){

RoutingStatementHandler statementHandler = (RoutingStatementHandler)invocation.getTarget();

StatementHandler delegate = (StatementHandler) CommonUtil.getFieldValue(statementHandler, "delegate");

BoundSql boundSql = delegate.getBoundSql();

Object obj = boundSql.getParameterObject();

if (obj instanceof PageDto) {

PageDto page = (PageDto) obj;

//獲取delegate父類BaseStatementHandler的mappedStatement屬性

MappedStatement mappedStatement = (MappedStatement)CommonUtil.getFieldValue(delegate, "mappedStatement");

//攔截到的prepare方法參數(shù)是一個Connection對象

Connection connection = (Connection)invocation.getArgs()[0];

//獲取當(dāng)前要執(zhí)行的Sql語句

String sql = boundSql.getSql();

//給當(dāng)前的page參數(shù)對象設(shè)置總記錄數(shù)

this.setTotalRecord(page, mappedStatement, connection);

//給當(dāng)前的page參數(shù)對象補(bǔ)全完整信息

//this.setPageInfo(page);

//獲取分頁Sql語句

String pageSql = this.getPageSql(page, sql);

//設(shè)置當(dāng)前BoundSql對應(yīng)的sql屬性為我們建立好的分頁Sql語句

CommonUtil.setFieldValue(boundSql, "sql", pageSql);

}

}

return invocation.proceed();

}

?

/**

* 給當(dāng)前的參數(shù)對象page設(shè)置總記錄數(shù)

*

* @param page Mapper映射語句對應(yīng)的參數(shù)對象

* @param mappedStatement Mapper映射語句

* @param connection 當(dāng)前的數(shù)據(jù)庫連接

*/

private void setTotalRecord(PageDto page, MappedStatement mappedStatement, Connection connection) throws Exception{

//獲取對應(yīng)的BoundSql

BoundSql boundSql = mappedStatement.getBoundSql(page);

//獲取對應(yīng)的Sql語句

String sql = boundSql.getSql();

//獲取計算總記錄數(shù)的sql語句

String countSql = this.getCountSql(sql);

//通過BoundSql獲取對應(yīng)的參數(shù)映射

List parameterMappings = boundSql.getParameterMappings();

//利用Configuration右钾、查詢記錄數(shù)的Sql語句countSql摧茴、參數(shù)映射關(guān)系parameterMappings和參數(shù)對象page建立查詢記錄數(shù)對應(yīng)的BoundSql對象容握。

BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);

//通過mappedStatement拷姿、參數(shù)對象page和BoundSql對象countBoundSql建立一個用于設(shè)定參數(shù)的ParameterHandler對象

ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);

//通過connection建立一個countSql對應(yīng)的PreparedStatement對象逆巍。

PreparedStatement pstmt = null;

ResultSet rs = null;

try {

pstmt = connection.prepareStatement(countSql);

//通過parameterHandler給PreparedStatement對象設(shè)置參數(shù)

parameterHandler.setParameters(pstmt);

//執(zhí)行獲取總記錄數(shù)的Sql語句。

rs = pstmt.executeQuery();

if (rs.next()) {

int totalRecord = rs.getInt(1);

//給當(dāng)前的參數(shù)page對象設(shè)置總記錄數(shù)

page.setTotalRecord(totalRecord);

}

} catch (SQLException e) {

log.error(e);

throw new SQLException();

} finally {

try {

if (rs != null)

rs.close();

if (pstmt != null)

pstmt.close();

} catch (SQLException e) {

log.error(e);

throw new SQLException();

}

}

}

?

/**

* 根據(jù)原Sql語句獲取對應(yīng)的查詢總記錄數(shù)的Sql語句

* @param sql 原sql

* @return 查詢總記錄數(shù)sql

*/

private String getCountSql(String sql) {

int index = new String(sql).toLowerCase().indexOf("from");

return "select count(*) " + sql.substring(index);

}

?

/**

* 給page對象補(bǔ)充完整信息

*

* @param page page對象

*/

private void setPageInfo(PageDto page) {

Integer totalRecord = page.getTotalRecord();

Integer pageNo = page.getPageNo();

Integer rows = page.getRows();

?

//設(shè)置總頁數(shù)

Integer totalPage;

if (totalRecord > rows) {

if (totalRecord % rows == 0) {

totalPage = totalRecord / rows;

} else {

totalPage = 1 + (totalRecord / rows);

}

} else {

totalPage = 1;

}

page.setTotalPage(totalPage);

?

//跳轉(zhuǎn)頁大于總頁數(shù)時,默認(rèn)跳轉(zhuǎn)至最后一頁

if (pageNo > totalPage) {

pageNo = totalPage;

page.setPageNo(pageNo);

}

?

//設(shè)置是否有前頁

if(pageNo <= 1) {

page.setHasPrevious(false);

} else {

page.setHasPrevious(true);

}

?

//設(shè)置是否有后頁

if(pageNo >= totalPage) {

page.setHasNext(false);

} else {

page.setHasNext(true);

}

}

?

/**

* 根據(jù)page對象獲取對應(yīng)的分頁查詢Sql語句

* 其它的數(shù)據(jù)庫都 沒有進(jìn)行分頁

*

* @param page 分頁對象

* @param sql 原sql語句

* @return 分頁sql

*/

private String getPageSql(PageDto page, String sql) {

StringBuffer sqlBuffer = new StringBuffer(sql);

if ("mysql".equalsIgnoreCase(dialect)) {

//int offset = (page.getPageNo() - 1) * page.getRows();

sqlBuffer.append(" limit ").append(page.getOffset()).append(",").append(page.getRows());

return sqlBuffer.toString();

}

return sqlBuffer.toString();

}

?

/**

* 攔截器對應(yīng)的封裝原始對象的方法

*/

@Override

public Object plugin(Object arg0) {

?

if (arg0 instanceof StatementHandler) {

return Plugin.wrap(arg0, this);

} else {

return arg0;

}

}

?

/**

* 設(shè)置注冊攔截器時設(shè)定的屬性

*/

@Override

public void setProperties(Properties p) {

?

}

?

public String getDialect() {

return dialect;

}

?

public void setDialect(String dialect) {

this.dialect = dialect;

}

?

}

重點講解:

@Intercept注解中的@Signature中標(biāo)示的屬性忿晕,標(biāo)示當(dāng)前攔截器要攔截的那個類的那個方法绍妨,攔截方法的傳入的參數(shù)

首先要明白志秃,Mybatis是對JDBC的一個高層次的封裝房待。而JDBC在完成數(shù)據(jù)操作的時候必須要有一個陳述對象邢羔。而陳述對應(yīng)的SQL語句是在是在陳之前產(chǎn)生的。所以我們的思路就是在生成報表之前對SQL進(jìn)行下手桑孩。更改SQL語句成我們需要的张抄!

對于MyBatis的,其聲明的英文生成在RouteStatementHandler中洼怔。所以我們要做的就是攔截這個處理程序的prepare方法!然后修改的Sql語句!

@Override

public Object intercept(Invocation invocation) throws Throwable {

// 其實就是代理模式左驾!

RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();

StatementHandler delegate = (StatementHandler)ReflectUtil.getFieldValue(handler, "delegate");

String sql= delegate.getBoundSql().getSql();

return invocation.proceed();

}

我們知道利用Mybatis查詢一個集合時傳入Rowbounds對象即可指定其Offset和Limit镣隶,只不過其沒有利用原生sql去查詢罷了,我們現(xiàn)在做的诡右,就是通過攔截器拿到這個參數(shù)安岂,然后織入到SQL語句中,這樣我們就可以完成一個物理分頁帆吻!

注冊攔截器

在Spring文件中引入攔截器


...


分頁定義的接口:

List selectForSearch(PageDto pageDto);

客戶端調(diào)用如下:

PageDto pageDto = new PageDto<>();

Student student =new Student();

student.setId(1234);

student.setName("sky");

pageDto.setSearchCondition(student);

如果想學(xué)習(xí)Java工程化域那、高性能及分布式、深入淺出猜煮。性能調(diào)優(yōu)次员、Spring,MyBatis王带,Netty源碼分析的朋友可以加我的Java高級架構(gòu)進(jìn)階群:180705916淑蔚,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費分享給大家

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末愕撰,一起剝皮案震驚了整個濱河市刹衫,隨后出現(xiàn)的幾起案子醋寝,更是在濱河造成了極大的恐慌,老刑警劉巖带迟,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件音羞,死亡現(xiàn)場離奇詭異,居然都是意外死亡仓犬,警方通過查閱死者的電腦和手機(jī)嗅绰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婶肩,“玉大人办陷,你說我怎么就攤上這事÷杉撸” “怎么了民镜?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長险毁。 經(jīng)常有香客問我制圈,道長,這世上最難降的妖魔是什么畔况? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任鲸鹦,我火速辦了婚禮,結(jié)果婚禮上跷跪,老公的妹妹穿的比我還像新娘馋嗜。我一直安慰自己,他們只是感情好吵瞻,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布葛菇。 她就那樣靜靜地躺著,像睡著了一般橡羞。 火紅的嫁衣襯著肌膚如雪眯停。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天卿泽,我揣著相機(jī)與錄音莺债,去河邊找鬼。 笑死签夭,一個胖子當(dāng)著我的面吹牛齐邦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播第租,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼侄旬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了煌妈?” 一聲冷哼從身側(cè)響起儡羔,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤宣羊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后汰蜘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仇冯,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年族操,在試婚紗的時候發(fā)現(xiàn)自己被綠了苛坚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡色难,死狀恐怖泼舱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情枷莉,我是刑警寧澤娇昙,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站笤妙,受9級特大地震影響冒掌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹲盘,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一股毫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧召衔,春花似錦铃诬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至毫深,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間毒姨,已是汗流浹背哑蔫。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留弧呐,地道東北人闸迷。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像俘枫,于是被迫代替她去往敵國和親腥沽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354