Mybatis插件原理

記錄是一種精神,是加深理解最好的方式之一。

最近看了下Mybatis的源碼,分析了Mybatis插件的實(shí)現(xiàn)方式,在這里把他記下來。
曹金桂 cao_jingui@163.com (如有欠缺還請(qǐng)指教)
時(shí)間:2016年10月23日16:00

概述

Mybatis插件又稱攔截器,本篇文章中出現(xiàn)的攔截器都表示插件

Mybatis采用責(zé)任鏈模式,通過動(dòng)態(tài)代理組織多個(gè)插件(攔截器),通過這些插件可以改變Mybatis的默認(rèn)行為(諸如SQL重寫之類的),由于插件會(huì)深入到Mybatis的核心,因此在編寫自己的插件前最好了解下它的原理,以便寫出安全高效的插件。

MyBatis 允許你在已映射語句執(zhí)行過程中的某一點(diǎn)進(jìn)行攔截調(diào)用。默認(rèn)情況下,MyBatis 允許使用插件來攔截的方法調(diào)用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

總體概括為:

  • 攔截執(zhí)行器的方法
  • 攔截參數(shù)的處理
  • 攔截結(jié)果集的處理
  • 攔截Sql語法構(gòu)建的處理

Mybatis是通過動(dòng)態(tài)代理的方式實(shí)現(xiàn)攔截的,閱讀此篇文章需要先對(duì)Java的動(dòng)態(tài)代理機(jī)制有所了解??梢詤⒖疾┛?a target="_blank" rel="nofollow">《徹底理解java動(dòng)態(tài)代理》

Mybatis四大接口

竟然Mybatis是對(duì)四大接口進(jìn)行攔截的,那我們藥先要知道Mybatis的四大接口對(duì)象 Executor, StatementHandler, ResultSetHandler, ParameterHandler。



上圖Mybatis框架的整個(gè)執(zhí)行過程。Mybatis插件能夠?qū)t四大對(duì)象進(jìn)行攔截,可以包含到了Mybatis一次會(huì)議的所有操作??梢奙ybatis的的插件很強(qiáng)大。

  1. Executor是 Mybatis的內(nèi)部執(zhí)行器,它負(fù)責(zé)調(diào)用StatementHandler操作數(shù)據(jù)庫,并把結(jié)果集通過 ResultSetHandler進(jìn)行自動(dòng)映射,另外,他還處理了二級(jí)緩存的操作。從這里可以看出,我們也是可以通過插件來實(shí)現(xiàn)自定義的二級(jí)緩存的。
  2. StatementHandler是Mybatis直接和數(shù)據(jù)庫執(zhí)行sql腳本的對(duì)象。另外它也實(shí)現(xiàn)了Mybatis的一級(jí)緩存。這里,我們可以使用插件來實(shí)現(xiàn)對(duì)一級(jí)緩存的操作(禁用等等)。
  3. ParameterHandler是Mybatis實(shí)現(xiàn)Sql入?yún)⒃O(shè)置的對(duì)象。插件可以改變我們Sql的參數(shù)默認(rèn)設(shè)置。
  4. ResultSetHandler是Mybatis把ResultSet集合映射成POJO的接口對(duì)象。我們可以定義插件對(duì)Mybatis的結(jié)果集自動(dòng)映射進(jìn)行修改。

插件Interceptor

Mybatis的插件實(shí)現(xiàn)要實(shí)現(xiàn)Interceptor接口,我們看下這個(gè)接口定義的方法。

public interface Interceptor {   
   Object intercept(Invocation invocation) throws Throwable;       
   Object plugin(Object target);    
   void setProperties(Properties properties);
}

這個(gè)接口只聲明了三個(gè)方法。

  • setProperties方法是在Mybatis進(jìn)行配置插件的時(shí)候可以配置自定義相關(guān)屬性,即:接口實(shí)現(xiàn)對(duì)象的參數(shù)配置
  • plugin方法是插件用于封裝目標(biāo)對(duì)象的,通過該方法我們可以返回目標(biāo)對(duì)象本身,也可以返回一個(gè)它的代理,可以決定是否要進(jìn)行攔截進(jìn)而決定要返回一個(gè)什么樣的目標(biāo)對(duì)象,官方提供了示例:return Plugin.wrap(target, this);
  • intercept方法就是要進(jìn)行攔截的時(shí)候要執(zhí)行的方法

理解這個(gè)接口的定義,先要知道java動(dòng)態(tài)代理機(jī)制。plugin接口即返回參數(shù)target對(duì)象(Executor/ParameterHandler/ResultSetHander/StatementHandler)的代理對(duì)象。在調(diào)用對(duì)應(yīng)對(duì)象的接口的時(shí)候,可以進(jìn)行攔截并處理。

Mybatis四大接口對(duì)象創(chuàng)建方法

Mybatis的插件是采用對(duì)四大接口的對(duì)象生成動(dòng)態(tài)代理對(duì)象的方法來實(shí)現(xiàn)的。那么現(xiàn)在我們看下Mybatis是怎么創(chuàng)建這四大接口對(duì)象的。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
   //確保ExecutorType不為空(defaultExecutorType有可能為空)
   executorType = executorType == null ? defaultExecutorType : executorType;
   executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
   Executor executor;   if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
   } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
   } else {
      executor = new SimpleExecutor(this, transaction);
   }   if (cacheEnabled) {
      executor = new CachingExecutor(executor);
   }
   executor = (Executor) interceptorChain.pluginAll(executor);
   return executor;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
   StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
   statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
   return statementHandler;
}

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
   ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
   parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
   return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
   ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
   resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
   return resultSetHandler;
}

查看源碼可以發(fā)現(xiàn), Mybatis框架在創(chuàng)建好這四大接口對(duì)象的實(shí)例后,都會(huì)調(diào)用InterceptorChain.pluginAll()方法。InterceptorChain對(duì)象是插件執(zhí)行鏈對(duì)象,看源碼就知道里面維護(hù)了Mybatis配置的所有插件(Interceptor)對(duì)象。

// target  --> Executor/ParameterHandler/ResultSetHander/StatementHandler
public Object pluginAll(Object target) {
   for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
   }
   return target;
}

其實(shí)就是安順序執(zhí)行我們插件的plugin方法,一層一層返回我們?cè)瓕?duì)象(Executor/ParameterHandler/ResultSetHander/StatementHandler)的代理對(duì)象。當(dāng)我們調(diào)用四大接口對(duì)象的方法時(shí)候,實(shí)際上是調(diào)用代理對(duì)象的響應(yīng)方法,代理對(duì)象又會(huì)調(diào)用十大接口對(duì)象的實(shí)例。

Plugin對(duì)象

我們知道,官方推薦插件實(shí)現(xiàn)plugin方法為:Plugin.wrap(target, this);

public static Object wrap(Object target, Interceptor interceptor) {
   // 獲取插件的Intercepts注解
   Map<Class<?>, Set<Method>> 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;
}

這個(gè)方法其實(shí)是Mybatis簡(jiǎn)化我們插件實(shí)現(xiàn)的工具方法。其實(shí)就是根據(jù)當(dāng)前攔截的對(duì)象創(chuàng)建了一個(gè)動(dòng)態(tài)代理對(duì)象。代理對(duì)象的InvocationHandler處理器為新建的Plugin對(duì)象。

插件配置注解@Intercepts

Mybatis的插件都要有Intercepts注解來指定要攔截哪個(gè)對(duì)象的哪個(gè)方法。我們知道,Plugin.warp方法會(huì)返回四大接口對(duì)象的代理對(duì)象(通過new Plugin()創(chuàng)建的IvocationHandler處理器),會(huì)攔截所有的執(zhí)行方法。在代理對(duì)象執(zhí)行對(duì)應(yīng)方法的時(shí)候,會(huì)調(diào)用InvocationHandler處理器的invoke方法。Mybatis中利用了注解的方式配置指定攔截哪些方法。具體如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   try {
      Set<Method> 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);
   }
}

可以看到,只有通過Intercepts注解指定的方法才會(huì)執(zhí)行我們自定義插件的intercept方法。未通過Intercepts注解指定的將不會(huì)執(zhí)行我們的intercept方法。

官方插件開發(fā)方式

@Intercepts({@Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class TestInterceptor implements Interceptor {
   public Object intercept(Invocation invocation) throws Throwable {
     Object target = invocation.getTarget(); //被代理對(duì)象
     Method method = invocation.getMethod(); //代理方法
     Object[] args = invocation.getArgs(); //方法參數(shù)
     // do something ...... 方法攔截前執(zhí)行代碼塊
     Object result = invocation.proceed();
     // do something .......方法攔截后執(zhí)行代碼塊
     return result;
   }
   public Object plugin(Object target) {
     return Plugin.wrap(target, this);
   }
}

以上就是Mybatis官方推薦的插件實(shí)現(xiàn)的方法,通過Plugin對(duì)象創(chuàng)建被代理對(duì)象的動(dòng)態(tài)代理對(duì)象。可以發(fā)現(xiàn),Mybatis的插件開發(fā)還是很簡(jiǎn)單的。

自定義開發(fā)方式

Mybatis的插件開發(fā)通過內(nèi)部提供的Plugin對(duì)象可以很簡(jiǎn)單的開發(fā)。只有理解了插件實(shí)現(xiàn)原理,對(duì)應(yīng)不采用Plugin對(duì)象我們一樣可以自己實(shí)現(xiàn)插件的開發(fā)。下面是我個(gè)人理解之后的自己實(shí)現(xiàn)的一種方式。

public class TestInterceptor implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget(); //被代理對(duì)象
        Method method = invocation.getMethod(); //代理方法
        Object[] args = invocation.getArgs(); //方法參數(shù)
        // do something ...... 方法攔截前執(zhí)行代碼塊
        Object result = invocation.proceed();
        // do something .......方法攔截后執(zhí)行代碼塊
        return result;
    }
    public Object plugin(final Object target) {
        return Proxy.newProxyInstance(Interceptor.class.getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return intercept(new Invocation(target, method, args));
            }
        });
    }
    public void setProperties(Properties properties) {
    }
}

當(dāng)然,Mybatis插件的那這個(gè)時(shí)候Intercepts的注解起不到作用了。

小結(jié)

我們?cè)贛yBatis配置了一個(gè)插件,在運(yùn)行發(fā)生了什么

  1. 所有可能被攔截的處理類都會(huì)生成一個(gè)代理
  2. 處理類代理在執(zhí)行對(duì)應(yīng)方法時(shí),判斷要不要執(zhí)行插件中的攔截方法
  3. 執(zhí)行插接中的攔截方法后,推進(jìn)目標(biāo)的執(zhí)行

如果有N個(gè)插件,就有N個(gè)代理,每個(gè)代理都要執(zhí)行上面的邏輯。這里面的層層代理要多次生成動(dòng)態(tài)代理,是比較影響性能的。雖然能指定插件攔截的位置,但這個(gè)是在執(zhí)行方法時(shí)動(dòng)態(tài)判斷,初始化的時(shí)候就是簡(jiǎn)單的把插件包裝到了所有可以攔截的地方。

因此,在編寫插件時(shí)需注意以下幾個(gè)原則:

  • 不編寫不必要的插件;
  • 實(shí)現(xiàn)plugin方法時(shí)判斷一下目標(biāo)類型,是本插件要攔截的對(duì)象才執(zhí)行Plugin.wrap方法,否者直接返回目標(biāo)本省,這樣可以減少目標(biāo)被代理的次數(shù)。
// 假如我們只要攔截Executor對(duì)象,那么我們應(yīng)該這么做
public Object plugin(final Object target) {
   if (target instanceof Executor) {
      return Plugin.wrap(target, this);
   } else {
      return target;
   }
}

Mybatis插件很強(qiáng)大,可以對(duì)Mybatis框架進(jìn)行很大的擴(kuò)展。當(dāng)然,如果你不理解Mybatis插件的原理,開發(fā)起來只能是模擬兩可。在實(shí)際開發(fā)過程中,我們可以參考別人寫的插件。下面是一個(gè)Mybatis分頁的插件,可以為以后開發(fā)做參考。

/**
 * Mybatis - 通用分頁插件(如果開啟二級(jí)緩存需要注意)
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class}),
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
@Log4j
public class PageHelper implements Interceptor {

    public static final ThreadLocal<Page> localPage = new ThreadLocal<Page>();

    /**
     * 開始分頁
     *
     * @param pageNum
     * @param pageSize
     */
    public static void startPage(int pageNum, int pageSize) {
        localPage.set(new Page(pageNum, pageSize));
    }

    /**
     * 結(jié)束分頁并返回結(jié)果,該方法必須被調(diào)用,否則localPage會(huì)一直保存下去,直到下一次startPage
     *
     * @return
     */
    public static Page endPage() {
        Page page = localPage.get();
        localPage.remove();
        return page;
    }

    public Object intercept(Invocation invocation) throws Throwable {
        if (localPage.get() == null) {
            return invocation.proceed();
        }
        if (invocation.getTarget() instanceof StatementHandler) {
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
            // 分離代理對(duì)象鏈(由于目標(biāo)類可能被多個(gè)插件攔截,從而形成多次代理,通過下面的兩次循環(huán)
            // 可以分離出最原始的的目標(biāo)類)
            while (metaStatementHandler.hasGetter("h")) {
                Object object = metaStatementHandler.getValue("h");
                metaStatementHandler = SystemMetaObject.forObject(object);
            }
            // 分離最后一個(gè)代理對(duì)象的目標(biāo)類
            while (metaStatementHandler.hasGetter("target")) {
                Object object = metaStatementHandler.getValue("target");
                metaStatementHandler = SystemMetaObject.forObject(object);
            }
            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
            //分頁信息if (localPage.get() != null) {
            Page page = localPage.get();
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
            // 分頁參數(shù)作為參數(shù)對(duì)象parameterObject的一個(gè)屬性
            String sql = boundSql.getSql();
            // 重寫sql
            String pageSql = buildPageSql(sql, page);
            //重寫分頁sql
            metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
            Connection connection = (Connection) invocation.getArgs()[0];
            // 重設(shè)分頁參數(shù)里的總頁數(shù)等
            setPageParameter(sql, connection, mappedStatement, boundSql, page);
            // 將執(zhí)行權(quán)交給下一個(gè)插件
            return invocation.proceed();
        } else if (invocation.getTarget() instanceof ResultSetHandler) {
            Object result = invocation.proceed();
            Page page = localPage.get();
            page.setResult((List) result);
            return result;
        }
        return null;
    }

    /**
     * 只攔截這兩種類型的
     * <br>StatementHandler
     * <br>ResultSetHandler
     *
     * @param target
     * @return
     */
    public Object plugin(Object target) {
        if (target instanceof StatementHandler || target instanceof ResultSetHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    public void setProperties(Properties properties) {

    }

    /**
     * 修改原SQL為分頁SQL
     *
     * @param sql
     * @param page
     * @return
     */
    private String buildPageSql(String sql, Page page) {
        StringBuilder pageSql = new StringBuilder(200);
        pageSql.append("select * from (");
        pageSql.append(sql);
        pageSql.append(" ) temp limit ").append(page.getStartRow());
        pageSql.append(" , ").append(page.getPageSize());
        return pageSql.toString();
    }

    /**
     * 獲取總記錄數(shù)
     *
     * @param sql
     * @param connection
     * @param mappedStatement
     * @param boundSql
     * @param page
     */
    private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
                                  BoundSql boundSql, Page page) {
        // 記錄總記錄數(shù)
        String countSql = "select count(0) from (" + sql + ") temp";
        PreparedStatement countStmt = null;
        ResultSet rs = null;
        try {
            countStmt = connection.prepareStatement(countSql);
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());
            setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());
            rs = countStmt.executeQuery();
            int totalCount = 0;
            if (rs.next()) {
                totalCount = rs.getInt(1);
            }
            page.setTotal(totalCount);
            int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);
            page.setPages(totalPage);
        } catch (SQLException e) {
            log.error("Ignore this exception", e);
        } finally {
            try {
                rs.close();
            } catch (SQLException e) {
                log.error("Ignore this exception", e);
            }
            try {
                countStmt.close();
            } catch (SQLException e) {
                log.error("Ignore this exception", e);
            }
        }
    }

    /**
     * 代入?yún)?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);
    }

    @Data //采用lombok插件編譯
    public static class Page<E> {
        private int pageNum;
        private int pageSize;
        private int startRow;
        private int endRow;
        private long total;
        private int pages;
        private List<E> result;

        public Page(int pageNum, int pageSize) {
            this.pageNum = pageNum;
            this.pageSize = pageSize;
            this.startRow = pageNum > 0 ? (pageNum - 1) * pageSize : 0;
            this.endRow = pageNum * pageSize;
        }

    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,701評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,691評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,694評(píng)論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,026評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,193評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,719評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,668評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,846評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,394評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,635評(píng)論 2 380

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

  • MyBatis提供了一種插件(plugin)的功能,雖然叫做插件,但其實(shí)這是攔截器功能。那么攔截器攔截MyBati...
    七寸知架構(gòu)閱讀 3,256評(píng)論 3 54
  • 1 引言# 本文主要講解JDBC怎么演變到Mybatis的漸變過程,重點(diǎn)講解了為什么要將JDBC封裝成Mybait...
    七寸知架構(gòu)閱讀 76,574評(píng)論 36 980
  • 很久沒有翻找自己的抽屜了,我一直有整理的習(xí)慣,但是現(xiàn)在不了,因?yàn)闆]有私人物品 沒有整理的必要,過去有些小心事,臉上...
    亭子文閱讀 345評(píng)論 0 0
  • 大概三年前 我思考一個(gè)問題 什么是讓別人關(guān)注你接受你 最直接的方式 一次聚會(huì) 你會(huì)看到形形色色的人 但你會(huì)明顯的發(fā)...
    許豆蔻閱讀 238評(píng)論 0 0
  • 這兩天,我對(duì)于“思行合一”成長(zhǎng)體系,是應(yīng)該花時(shí)間去推廣運(yùn)營(yíng),還是應(yīng)該以其他東西踐行,產(chǎn)生了一些糾結(jié)和矛盾。 之所以...
    Tom教練閱讀 363評(píng)論 1 5