Mybatis源碼閱讀(四):核心接口4.2——Executor(上)

*************************************優雅的分割線 **********************************

分享一波:程序員賺外快-必看的巔峰干貨

如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程

請關注微信公眾號:HB荷包
在這里插入圖片描述
一個能讓你學習技術和賺錢方法的公眾號,持續更新
*************************************優雅的分割線 **********************************
Executor

Executor是Mybatis的核心接口之一,其中定義了數據庫操作的基本方法。在實際應用中涉及的SqlSession的操作都是基于Executor實現的。Executor代碼如下。

/**

  • Mybatis的核心接口,定義了操作數據庫的方法

  • SqlSession接口的功能都是基于Executor實現的

  • @author Clinton Begin
    */
    public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    /**

    • 執行update、insert、delete語句
    • @param ms
    • @param parameter
    • @return
    • @throws SQLException
      */
      int update(MappedStatement ms, Object parameter) throws SQLException;

    /**

    • 執行select
    • @param ms
    • @param parameter
    • @param rowBounds
    • @param resultHandler
    • @param cacheKey
    • @param boundSql
    • @param
    • @return
    • @throws SQLException
      */
      List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

    /**

    • 執行select
    • @param ms
    • @param parameter
    • @param rowBounds
    • @param resultHandler
    • @param
    • @return
    • @throws SQLException
      */
      List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

    /**

    • 執行select,返回游標
    • @param ms
    • @param parameter
    • @param rowBounds
    • @param
    • @return
    • @throws SQLException
      */
      Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

    /**

    • 批量執行SQL語句
    • @return
    • @throws SQLException
      */
      List flushStatements() throws SQLException;

    /**

    • 提交事務
    • @param required
    • @throws SQLException
      */
      void commit(boolean required) throws SQLException;

    /**

    • 回滾事務
    • @param required
    • @throws SQLException
      */
      void rollback(boolean required) throws SQLException;

    /**

    • 創建緩存中的CacheKey對象
    • @param ms
    • @param parameterObject
    • @param rowBounds
    • @param boundSql
    • @return
      */
      CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

    /**

    • 根據CacheKey查找緩存是否出在
    • @param ms
    • @param key
    • @return
      */
      boolean isCached(MappedStatement ms, CacheKey key);

    /**

    • 清除一級緩存
      */
      void clearLocalCache();

    /**

    • 延遲加載一級緩存中的數據
    • @param ms
    • @param resultObject
    • @param property
    • @param key
    • @param targetType
      */
      void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

    /**

    • 獲取事務對象
    • @return
      */
      Transaction getTransaction();

    /**

    • 關閉Executor對象
    • @param forceRollback
      */
      void close(boolean forceRollback);

    /**

    • 檢測Executor是否關閉
    • @return
      */
      boolean isClosed();

    /**

    • 設置包裝的Executor
    • @param executor
      */
      void setExecutorWrapper(Executor executor);

}

[點擊并拖拽以移動]

Executor接口的實現中使用到了裝飾器模式和模板方法模式,關于設計模式的內容可以查看我之前的文章,這里就不貼出文章鏈接了。Executor的實現如圖所示。

BaseExecutor

BaseExecutor是個抽象類,實現了Executor大部分的方法。BaseExecutor中主要提供了緩存管理和事務管理的基本功能,繼承BaseExecutor的子類只需要實現四個基本的方法來完成數據庫的相關操作即可,分別是doUpdate、doQuery、doQueryCursor、doFlushStatement。其余的方法在BaseExecutor中都有了實現。BaseExecutor的字段如下

/*** 事務對象*/
protected Transaction transaction;/*** 封裝的Executor對象*/
protected Executor wrapper;/*** 延遲加載隊列*/
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;/*** 一級緩存,用于緩存該Executor對象查詢結果集映射得到的結果對象*/
protected PerpetualCache localCache;/*** 一級緩存,用來緩存輸出類型的參數*/
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;/*** 記錄嵌套查詢的層數*/
protected int queryStack;
/*** 標識Executor是否關閉*/
private boolean closed;

一級緩存

常見的系統中,數據庫資源是比較珍貴的,在web系統中的性能瓶頸主要也就是數據庫。在設計系統時,會使用多種優化手段去減少數據庫的直接訪問,比如使用緩存。使用緩存可以減少系統與數據庫的網絡交互、減少數據庫訪問次數、降低數據庫負擔、降低重復創建和銷毀對象等一系列的開銷,從而提升系統的性能。同時,當數據庫意外宕機時,緩存中保存的數據可以繼續支持系統部分功能的正常展示,提高系統的可用性。Mybatis提供了一級緩存和二級緩存,我們這里先討論一級緩存。

一級緩存是會話級別的緩存,在Mybatis中每創建一個SqlSession對象,就表示開啟一次數據庫會話。在一次會話中,系統可能回反復的執行相同的查詢語句,如果不對數據庫進行緩存,那么短時間內執行多次完全相同的SQL語句,查詢到的結果集也可能完全相同,就造成了數據庫資源的浪費。

為了避免這種問題,Executor對象中會建立一個簡單的緩存,也就是一級緩存。它會將每次查詢結果緩存起來,再執行查詢操作時,會先查詢一級緩存,如果存在完全一樣的查詢語句,則直接從一級緩存中取出相應的結果對象返回給用戶,從而減少數據庫壓力。

一級緩存的生命周期與SqlSession相同,也就與SqlSession封裝的Executor對象的生命周期相同,當調用了Executor的close方法時,該Executor中的一級緩存將會不可用。同時,一級緩存中對象的存活時間也會受其他因素影響,比如在執行update方法時,也會先清空一級緩存。
query

BaseExecutor方法會首先創建CacheKey對象,并根據CacheKey對象查找一級緩存,如果緩存命中則直接返回緩存中記錄的結果對象。如果沒有命中則查詢數據庫得到結果集,之后將結果集映射成對象保存到一級緩存中,同時返回結果對象。query方法如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 獲取BoundSql對象BoundSql boundSql = ms.getBoundSql(parameter);// 創建CacheKey對象CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

在query方法中會先獲取到boundSql對象,并且去創建CacheKey對象,再調用query的一個重載方法。

這里的CacheKey由MappedStatement的id、對應的offset和limit、包含問號的sql語句、用戶傳遞的實參、Environment的id五部分構成,代碼如下。

/*** 創建CacheKey對象* CacheKey由Sql節點的id、offset、limit、sql、實參、環境組成** @param ms* @param parameterObject* @param rowBounds* @param boundSql* @return*/
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}CacheKey cacheKey = new CacheKey();// 將sql節點的id添加到CacheKeycacheKey.update(ms.getId());// 將offset添加到CacheKeycacheKey.update(rowBounds.getOffset());// 將limit添加到CacheKeycacheKey.update(rowBounds.getLimit());// 將SQL添加到CacheKey(包含?的sql)cacheKey.update(boundSql.getSql());// 獲取參數映射List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();// 獲取類型處理器TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// 遍歷參數映射for (ParameterMapping parameterMapping : parameterMappings) {// 輸出類型參數不要if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;// 獲取屬性名稱String propertyName = parameterMapping.getProperty();// 獲取參數值if (boundSql.hasAdditionalParameter(propertyName)) {value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 將實參參數值添加到CacheKeycacheKey.update(value);}}// 環境不為空if (configuration.getEnvironment() != null) {// 將當前環境添加到CacheKeycacheKey.update(configuration.getEnvironment().getId());}return cacheKey;
}

而query的重載方法會根據創建的CacheKey對象查詢一級緩存。如果緩存命中則將緩存中記錄的結果對象返回,如果未命中,則調用doQuery方法查詢數據庫,并存到一級緩存。代碼如下。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 存入到錯誤上下文中,便于后面操作異常ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());if (closed) {throw new ExecutorException("Executor was closed.");}// 非嵌套查詢并且當前select節點配置了flushCacheif (queryStack == 0 && ms.isFlushCacheRequired()) {// 先清空緩存clearLocalCache();}List<E> list;try {// 查詢層數+1queryStack++;// 先查詢 一級緩存list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) {// 針對存儲過程調用的處理。在一級緩存 命中時,獲取緩存中保存的輸出類型參數,設置到用戶傳入的實參中handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 數據庫查詢,并得到映射后的結果對象list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {// 當前查詢完成,查詢層數減少queryStack--;}// 延遲加載相關if (queryStack == 0) {// 觸發DeferredLoad加載一級緩存中記錄的嵌套查詢的結果對象for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// 加載完成后清除deferredLoadsdeferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// 根據localCacheScope配置決定是否清空一級緩存clearLocalCache();}}return list;
}

BaseExecutor中緩存除了緩存結果集以外,在分析嵌套查詢時,如果一級緩存中緩存了嵌套查詢的結果對象,則可以從一級緩存中直接加載該結果對象。如果一級緩存中記錄的嵌套查詢的結果對象并未完全加載,則可以通過DeferredLoad實現類實現延遲加載的功能。與這個流程相關的方法有兩個,isCached方法負責檢測是否緩存了指定查詢的結果對象,deferLoad方法負責創建DeferredLoad對象并添加到deferredLoad集合中。代碼如下。

/*** 檢測是否緩存了指定查詢的結果對象** @param ms* @param key* @return*/
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {// 檢測緩存中是否花奴才能了CacheKey對象return localCache.getObject(key) != null;
}/*** 負責創建DeferredLoad對象并將其添加到deferredLoads集合中** @param ms* @param resultObject* @param property* @param key* @param targetType*/
@Override
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {if (closed) {throw new ExecutorException("Executor was closed.");}DeferredLoad deferredLoad = new DeferredLoad(resultObject, property, key, localCache, configuration, targetType);if (deferredLoad.canLoad()) {// 一級緩存中已經記錄了指定查詢結果的對象,直接從緩存中加載對象,并設置到外層對象deferredLoad.load();} else {// 將deferredLoad對象添加到deferredLoads隊列中,待整個外層查詢結束后再加載結果對象deferredLoads.add(new DeferredLoad(resultObject, property, key, localCache, configuration, targetType));}
}

DeferredLoad是定義在BaseExecutor中的內部類,它負責從loadCache緩存中延遲加載結果對象,含義如下。

    /*** 外層對象對應的MetaObject*/private final MetaObject resultObject;/*** 延遲加載的屬性名稱*/private final String property;/*** 延遲加載的屬性類型*/private final Class<?> targetType;/*** 延遲加載的結果對象在一級緩存中的CacheKey*/private final CacheKey key;/*** 一級緩存*/private final PerpetualCache localCache;private final ObjectFactory objectFactory;/*** 負責結果對象的類型轉換*/private final ResultExtractor resultExtractor;

DeferredLoad的canLoad方法負責檢測緩存項是否已經完全加載到緩存中。BaseExecutor的queryFromDatabase方法中,開始調用doQuery查詢數據庫之前,會先在localCache中放一個占位符,待查詢完畢后會將key替換成真實的數據,此時緩存就完全加載了。queryFromDatabase方法的實現如下。

/*** 從數據庫中查詢** @param ms* @param parameter* @param rowBounds* @param resultHandler* @param key* @param boundSql* @param <E>* @return* @throws SQLException*/
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// 先添加一個占位符,查詢完畢后才將真正的結果對象放入緩存,此時算完全家在localCache.putObject(key, EXECUTION_PLACEHOLDER);try {list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {// 刪除占位符localCache.removeObject(key);}// 將真正的結果對象添加到一級緩存中localCache.putObject(key, list);// 如果是存儲過程if (ms.getStatementType() == StatementType.CALLABLE) {// 緩存輸出類型的參數localOutputParameterCache.putObject(key, parameter);}return list;
}

canLoad和load方法實現如下。

    /*** 判斷是否是完全加載** @return*/public boolean canLoad() {return localCache.getObject(key) != null && localCache.getObject(key) != EXECUTION_PLACEHOLDER;}/*** 負責從緩存中加載結果對象,設置到外層對象 的屬性中*/@SuppressWarnings("unchecked")public void load() {// 從緩存中查詢指定的結果對象List<Object> list = (List<Object>) localCache.getObject(key);// 將緩存的結果對象轉換成指定的類型Object value = resultExtractor.extractObjectFromList(list, targetType);// 設置到外層對象的對應屬性resultObject.setValue(property, value);}

clearLocalCache方法用于清空緩存。query方法會根據flushCache屬性和localCacheScope配置決定是否清空一級緩存。update方法在執行insert、update、delete三類SQL語句之前,會清空緩存。代碼比較簡單這里就不貼了。
事務操作

在BatchExecutor中可以緩存多條SQL,等待合適的時機將緩存的多條SQL一起發送給數據庫執行。Executor.flushStatements方法主要是針對批處理多條SQL語句的,會調用doFlushStatements方法處理Executor中緩存的多條SQL語句,在BaseExecutor的commit、rollback方法中會首先調用flushStatement方法,再執行相關事務操作,方法具體的實現如下。

public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {if (closed) {throw new ExecutorException("Executor was closed.");}return doFlushStatements(isRollBack);
}

BaseExecutor.commit方法首先會清空一級緩存,調用flushStatements,最后才根據參數決定是否真正提交事務。代碼如下,

/*** 提交事務* @param required* @throws SQLException*/
@Override
public void commit(boolean required) throws SQLException {if (closed) {throw new ExecutorException("Cannot commit, transaction is already closed");}// 清除緩存clearLocalCache();// 處理緩存的SQLflushStatements();if (required) {// 提交事務transaction.commit();}
}

*************************************優雅的分割線 **********************************

分享一波:程序員賺外快-必看的巔峰干貨

如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程

請關注微信公眾號:HB荷包
在這里插入圖片描述
一個能讓你學習技術和賺錢方法的公眾號,持續更新
*************************************優雅的分割線 **********************************

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:
http://www.pswp.cn/news/536964.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/536964.shtml
英文地址,請注明出處:http://en.pswp.cn/news/536964.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

接收xml參數_SpringBoot實戰(二):接收xml請求

強烈推薦一個大神的人工智能的教程&#xff1a;http://www.captainbed.net/zhanghan【前言】最近在對接一個第三方系統&#xff0c;需要接收第三方系統的回調&#xff0c;而且格式為XML形式&#xff0c;之前自己一般接收的參數是Json形式&#xff0c;于是乎做個實驗驗證一下使用…

報錯 插入更新_window如何解決mysql數據量過大導致的報錯

window如何解決報錯“The total number of locks exceeds the lock table size”第一大步&#xff0c;查看mysql配置信息在CMD中輸入mysql -hlocalhost -uroot -p #如果設置了密碼直接接在p 后面 show variables like %storage_engine%以下為結果可以看到InnoDB是MySQL的默認引…

148. Sort List

Sort a linked list in O(n log n) time using constant space complexity. Example 1: Input: 4->2->1->3 Output: 1->2->3->4 Example 2: Input: -1->5->3->4->0 Output: -1->0->3->4->5難度&#xff1a;medium 題目&#xff1a;排…

Mybatis源碼閱讀(四):核心接口4.2——Executor(下)

*************************************優雅的分割線 ********************************** 分享一波:程序員賺外快-必看的巔峰干貨 如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程 請關注微信公眾號:HB荷包 一個能讓你學習技術和賺錢方法的公眾號,持續更…

python解橢圓方程的例題_橢圓標準方程典型例題及練習題

橢圓標準方程典型例題例1已知P 點在以坐標軸為對稱軸的橢圓上&#xff0c;點P 到兩焦點的距離分別為354和352&#xff0c;過P 點作焦點所在軸的垂線&#xff0c;它恰好過橢圓的一個焦點&#xff0c;求橢圓方程&#xff0e; 解&#xff1a;設兩焦點為1F 、2F &#xff0c;且3541…

leetcode393. UTF-8 Validation

題目要求 A character in UTF8 can be from 1 to 4 bytes long, subjected to the following rules:For 1-byte character, the first bit is a 0, followed by its unicode code. For n-bytes character, the first n-bits are all ones, the n1 bit is 0, followed by n-1 by…

Mybatis源碼閱讀(五 ):接口層——SqlSession

*************************************優雅的分割線 ********************************** 分享一波:程序員賺外快-必看的巔峰干貨 如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程 請關注微信公眾號:HB荷包 一個能讓你學習技術和賺錢方法的公眾號,持續更…

插入公式_一個小工具,徹底幫你搞定在Markdown中插入公式的問題

在編輯Markdown文檔時&#xff0c;插入公式是一個挺麻煩的活兒。需要掌握LaTex語法。我自己看完語法后&#xff0c;直接放棄&#xff0c;這絕對是反人類的語法。&#xff08;好吧&#xff0c;是我不會用...&#xff09;但是&#xff0c;我相信你看了這篇文章后&#xff0c;絕對…

JavaScript數據結構與算法——字典

1.字典數據結構 在字典中&#xff0c;存儲的是【鍵&#xff0c;值】對&#xff0c;其中鍵名是用來查詢特定元素的。字典和集合很相似&#xff0c;集合以【值&#xff0c;值】的形式存儲&#xff0c;字典則是用【鍵&#xff0c;值】對的形式存儲。字典也稱作映射。 2.創建字典 f…

Mybatis源碼閱讀(一):Mybatis初始化1.2 —— 解析別名、插件、對象工廠、反射工具箱、環境

*************************************優雅的分割線 ********************************** 分享一波:程序員賺外快-必看的巔峰干貨 如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程 請關注微信公眾號:HB荷包 一個能讓你學習技術和賺錢方法的公眾號,持續更…

中西方對時間的差異_中西方時間觀念差異 英文

The concept of time(時間觀念)①Inchina&#xff0c;words and phrases about time are very general. Forexample&#xff0c;ifyoudatewithsomeone,mostofChineseusedtoanswer: in the afternoon /at night/after a while and so on.Butinwestern,peoplehaveaverystrongconc…

Google 修改 Chrome API,防止隱身模式檢測

開發四年只會寫業務代碼&#xff0c;分布式高并發都不會還做程序員&#xff1f; 在使用 Chrome 瀏覽網頁時&#xff0c;某些網站會使用某種方法來確定訪問者是否處于隱身模式&#xff0c;這是一種隱私泄漏行為。Google 目前正在考慮修改 Chrome 的相關 API&#xff0c;來杜絕…

Mybatis源碼閱讀(一):Mybatis初始化1.1 解析properties、settings

*************************************優雅的分割線 ********************************** 分享一波:程序員賺外快-必看的巔峰干貨 如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程 請關注微信公眾號:HB荷包 一個能讓你學習技術和賺錢方法的公眾號,持續更…

亞馬遜推薦python_使用python查找amazon類別

我想得到amazon的類別&#xff0c;我計劃廢棄不用API。我已經取消了http://www.amazon.com&#xff0c;我已經在Shop By Department下拉列表中抓取了所有的類別和子類別&#xff0c;我創建了一個web服務來完成這項工作&#xff0c;代碼就在這里route(/hello)def hello():textli…

JavaScript異步基礎

唯一比不知道代碼為什么崩潰更可怕的事情是&#xff0c;不知道為什么一開始它是工作的&#xff01;在 ECMA 規范的最近幾次版本里不斷有新成員加入&#xff0c;尤其在處理異步的問題上&#xff0c;更是不斷推陳出新。然而&#xff0c;我們在享受便利的同時&#xff0c;也應該了…

Flutter、ReactNative、uniapp對比

*************************************優雅的分割線 ********************************** 分享一波:程序員賺外快-必看的巔峰干貨 如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程 請關注微信公眾號:HB荷包 一個能讓你學習技術和賺錢方法的公眾號,持續更…

JavaScript數組方法

一、基本類型和引用類型 數值、字符串、布爾值、undefined、null可以直接寫出來&#xff0c;比較簡單的數據稱為基本類型&#xff0c;在比較的時候&#xff0c;是直接按值比較。對象、函數、數組復雜的數據是引用類型&#xff0c;在比較的時候&#xff0c;是按照地址比較。cons…

nodejs mysql模塊_NodeJs使用Mysql模塊實現事務處理

依賴模塊&#xff1a;1. mysql&#xff1a;https://github.com/felixge/node-mysqlnpm install mysql --save2. async&#xff1a;https://github.com/caolan/asyncnpm install async --save(ps: async模塊可換成其它Promise模塊如bluebird、q等)因為Node.js的mysql模塊本身對于…

計數排序vs基數排序vs桶排序

從計數排序說起 計數排序是一種非基于元素比較的排序算法&#xff0c;而是將待排序數組元素轉化為計數數組的索引值&#xff0c;從而間接使待排序數組具有順序性。 計數排序的實現一般有兩種形式&#xff1a;基于輔助數組和基于桶排序。 基于輔助數組 整個過程包含三個數組&…

多線程中ThreadLocal的使用

*************************************優雅的分割線 ********************************** 分享一波:程序員賺外快-必看的巔峰干貨 如果以上內容對你覺得有用,并想獲取更多的賺錢方式和免費的技術教程 請關注微信公眾號:HB荷包 一個能讓你學習技術和賺錢方法的公眾號,持續更…