Spring 事務(wù)原理

Spring事務(wù)攔截器為

org.springframework.transaction.interceptor.TransactionInterceptor

當(dāng)Spring對(duì)函數(shù)進(jìn)行事務(wù)攔截時(shí)装黑,會(huì)調(diào)用到TransactionInterceptor的invoke函數(shù)蛔外。

@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

    // 在事務(wù)中調(diào)用函數(shù)瘦赫,使函數(shù)支持事務(wù)
    return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
        @Override
        public Object proceedWithInvocation() throws Throwable {
            return invocation.proceed();
        }
    });
}

invokeWithTransaction函數(shù)

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
        throws Throwable {

    // 數(shù)據(jù)準(zhǔn)備
    final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    final String joinpointIdentification = methodIdentification(method, targetClass);

    if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 1.新建一個(gè)事務(wù)
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
        Object retVal = null;
        try {
            //2.執(zhí)行業(yè)務(wù)代碼
            retVal = invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            // 異常處理
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {//
            cleanupTransactionInfo(txInfo);
        }
        //3.提交事務(wù)
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
//......其他代碼
}

整個(gè)事務(wù)處理過程中主要涉及到四個(gè)類

  1. TransactionAttribute
  2. PlatformTransactionManager
  3. TransactionStatus
  4. TransactionInfo

TransactionAttribute

public interface TransactionAttribute extends TransactionDefinition {
String getQualifier();
boolean rollbackOn(Throwable ex);
}

父類TransactionDefinition的定義

public interface TransactionDefinition {
  int getIsolationLevel();
  String getName();
  int getPropagationBehavior();
  boolean isReadOnly();
  int getTimeout();
}

TransactionAttribute用于保存@Transactional注解上的屬性內(nèi)容称近。

PlatformTransactionManager

package org.springframework.transaction;
public interface PlatformTransactionManager {
  //啟動(dòng)一個(gè)新事務(wù)
  TransactionStatus getTransaction(TransactionDefinition definition)
  //提交事務(wù)
  void commit(TransactionStatus status)
  //回滾事務(wù)
  void rollback(TransactionStatus status)
  }

PlatformTransactionManager是事務(wù)的具體操作類蓝牲,類似于JDBC的事務(wù)操作赌莺,使用PlatformTransactionManager進(jìn)行事務(wù)處理的偽代碼如下:

PlatformTransactionManager pm;
TransactionDefinition definition
//啟動(dòng)事務(wù)
TransactionStatus  status=pm.getTransaction(definition);
  try{
  //處理業(yè)務(wù)邏輯
  .....
  //提交事務(wù)
  pm.commit(status);
  }catch(Throwable e){
    //回滾事務(wù)
    pm.rollback(status);
  }
}

**TransactionStatus **

public interface TransactionStatus{
  boolean isNewTransaction();
  boolean hasSavepoint();
  void setRollbackOnly();
  boolean isRollbackOnly();
  void flush();
  boolean isCompleted();
}

TransactionStatus表征一次事務(wù)操作司志。

TransactionInfo

protected final class TransactionInfo {

    private final PlatformTransactionManager transactionManager;

    private final TransactionAttribute transactionAttribute;

    private final String joinpointIdentification;

    private TransactionStatus transactionStatus;

    private TransactionInfo oldTransactionInfo;
}

TransactionInfo將上面的三個(gè)類的內(nèi)容融合在一起,即當(dāng)前事務(wù)的相關(guān)信息降宅,
oldTransactionInfo為父事務(wù)的相關(guān)信息骂远。

Spring的事務(wù)的具體操作是PlatformTransactionManager,下面來(lái)具體說明PlatformTransactionManager得三個(gè)函數(shù)腰根。
1. getTransaction

    TransactionStatus org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(TransactionDefinition definition)

getTransaction函數(shù)的業(yè)務(wù)邏輯可簡(jiǎn)化為

    public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
        //1.獲取當(dāng)前的事務(wù)對(duì)象激才,如數(shù)據(jù)庫(kù)的connection對(duì)象
        Object transaction = doGetTransaction();
        //2.如果當(dāng)前的事務(wù)對(duì)象可用,根據(jù)當(dāng)前的傳遞方式额嘿,構(gòu)建TransactionStatus
        if (isExistingTransaction(transaction)) {
            return handleExistingTransaction(definition, transaction, debugEnabled);
        }//3.如果不存在瘸恼,根據(jù)當(dāng)前的傳遞方式,構(gòu)建TransactionStatus
        else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {

            DefaultTransactionStatus status = newTransactionStatus(
                    definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
            return status;
            
        }
    }

doGetTransaction函數(shù)
以PlatformTransactionManager的實(shí)現(xiàn)之一DataSourceTransactionManager為例册养,

    protected Object doGetTransaction() {
        DataSourceTransactionObject txObject = new DataSourceTransactionObject();
        txObject.setSavepointAllowed(isNestedTransactionAllowed());
        ConnectionHolder conHolder =
                (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);
        txObject.setConnectionHolder(conHolder, false);
        return txObject;
    }

TransactionSynchronizationManager.getResource是從線程上下文獲取數(shù)據(jù)庫(kù)連接信息东帅,如果存在,直接返回球拦,否則返回null.

isExistingTransaction
判斷當(dāng)前事務(wù)對(duì)象是否可用靠闭,在DataSourceTransactionManager中,只需要簡(jiǎn)單判斷ConnectionHolder是否可用即可刘莹。

    protected boolean isExistingTransaction(Object transaction) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
        return (txObject.getConnectionHolder() != null && txObject.getConnectionHolder().isTransactionActive());
    }

handleExistingTransaction
是處理事務(wù)傳遞的關(guān)鍵類阎毅,在handleExistingTransaction中會(huì)根據(jù)當(dāng)前不同的事務(wù)傳遞方式創(chuàng)建不同的TransactionStatus

1.PROPAGATION_NEVER
當(dāng)前方法不應(yīng)在Transaction中運(yùn)行,如果存在已經(jīng)定義的Transaction則拋出異常点弯。

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
        throw new IllegalTransactionStateException(
                "Existing transaction found for transaction marked with propagation 'never'");
    }

所以代碼中直接拋出異常

2.PROPAGATION_NOT_SUPPORTED
當(dāng)前方法不應(yīng)在Transaction中運(yùn)行扇调,如果存在已經(jīng)定義的Transaction,則該Transaction暫停(掛起)直至該方法運(yùn)行完畢抢肛。

    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
        if (debugEnabled) {
            logger.debug("Suspending current transaction");
        }
        Object suspendedResources = suspend(transaction);
        boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
        return prepareTransactionStatus(
                definition, null, false, newSynchronization, debugEnabled, suspendedResources);
    }

前面介紹過transaction是從上下文中獲取的狼钮,suspend函數(shù)掛起transaction的主要作用是將transaction的ConnectionHolder和事務(wù)的相關(guān)信息從線程上下文移除,這樣在業(yè)務(wù)代碼中獲取數(shù)據(jù)庫(kù)連接時(shí)捡絮,就不能從線程上下文中獲取到父事務(wù)的連接熬芜。

DefaultTransactionStatus prepareTransactionStatus(
        TransactionDefinition definition, Object transaction, boolean newTransaction,
        boolean newSynchronization, boolean debug, Object suspendedResources)

prepareTransactionStatus的作用有兩個(gè)

  1. 組裝DefaultTransactionStatus
  2. 如果newTransaction==true,更新當(dāng)前線程上下文福稳,保存當(dāng)前事務(wù)的信息涎拉,如事務(wù)隔離級(jí)別,傳遞方式等的圆。newTransaction還有另外一個(gè)作用:表示當(dāng)前事務(wù)需不需要進(jìn)行提交鼓拧,回滾操作。

當(dāng)傳遞方式為PROPAGATION_NOT_SUPPORTED時(shí)越妈,prepareTransactionStatus函數(shù)newTransaction參數(shù)的值為false季俩,不會(huì)更新線程上下文信息。業(yè)務(wù)代碼中獲取數(shù)據(jù)庫(kù)連接時(shí)會(huì)從數(shù)據(jù)源中獲取一個(gè)全新的connection使用梅掠。

3.Propagation.REQUIRES_NEW
當(dāng)前方法必須在新開的Transaction中運(yùn)行酌住。如果存在已經(jīng)定義的Transaction店归,則該已定義的Transaction暫停直至新開的Transaction執(zhí)行完畢。

    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
        SuspendedResourcesHolder suspendedResources = suspend(transaction);
        try {
            boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
            DefaultTransactionStatus status = newTransactionStatus(
                    definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
            doBegin(transaction, definition);
            prepareSynchronization(status, definition);
            return status;
        }
        catch (RuntimeException beginEx) {
            resumeAfterBeginException(transaction, suspendedResources, beginEx);
            throw beginEx;
        }
        catch (Error beginErr) {
            resumeAfterBeginException(transaction, suspendedResources, beginErr);
            throw beginErr;
        }
    }

操作分為3步:

  1. 掛起當(dāng)前事務(wù)
  2. 從數(shù)據(jù)源中獲取一個(gè)新的連接酪我,將connection的AutoCommit設(shè)置false.并將該connection綁定到當(dāng)前線程(doBegin函數(shù))消痛,以便業(yè)務(wù)代碼能獲取到該connection。
  3. 更新線程上下文關(guān)于當(dāng)前事務(wù)的相關(guān)信息都哭。

........
其他傳遞方式略

2.commit
commit 就比較簡(jiǎn)單肄满,主要做兩件事

  1. 如果當(dāng)前事務(wù)是newTransaction,提交事務(wù)
  2. 恢復(fù)當(dāng)前connection的屬性质涛,釋放connection
  3. 恢復(fù)被掛起的事務(wù)

提交connection

    protected void doCommit(DefaultTransactionStatus status) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Committing JDBC transaction on Connection [" + con + "]");
        }
        try {
            //數(shù)據(jù)庫(kù)connection提交
            con.commit();
        }
        catch (SQLException ex) {
            throw new TransactionSystemException("Could not commit JDBC transaction", ex);
        }
    }

恢復(fù)connection屬性,釋放connection

    protected void doCleanupAfterCompletion(Object transaction) {
        DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;

        // Remove the connection holder from the thread, if exposed.
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.unbindResource(this.dataSource);
        }

        // Reset connection.
        Connection con = txObject.getConnectionHolder().getConnection();
        try {
            if (txObject.isMustRestoreAutoCommit()) {
                con.setAutoCommit(true);
            }
            DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
        }
        catch (Throwable ex) {
            logger.debug("Could not reset JDBC Connection after transaction", ex);
        }

        if (txObject.isNewConnectionHolder()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Releasing JDBC Connection [" + con + "] after transaction");
            }
            DataSourceUtils.releaseConnection(con, this.dataSource);
        }

        txObject.getConnectionHolder().clear();
    }

恢復(fù)被掛起的事務(wù)

    protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder)
            throws TransactionException {

        if (resourcesHolder != null) {
            Object suspendedResources = resourcesHolder.suspendedResources;
            if (suspendedResources != null) {
                doResume(transaction, suspendedResources);
            }
            List<TransactionSynchronization> suspendedSynchronizations = resourcesHolder.suspendedSynchronizations;
            if (suspendedSynchronizations != null) {
                TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive);
                TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel);
                TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly);
                TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name);
                doResumeSynchronization(suspendedSynchronizations);
            }
        }
    }

3.rollback
與commit雷同掰担,不做解釋汇陆。

其他

Spring的Transactional注解是如何在mybatis中生效的?
Spring通過控制mybatis獲取的connection來(lái)起作用带饱。

mybatis的數(shù)據(jù)庫(kù)連接是從Mybatis的事務(wù)中獲取的

    package org.apache.ibatis.executor;
    public abstract class BaseExecutor{
        protected Connection getConnection(Log statementLog) throws SQLException {
            Connection connection = transaction.getConnection();
            if (statementLog.isDebugEnabled()) {
              return ConnectionLogger.newInstance(connection, statementLog, queryStack);
            } else {
              return connection;
            }
        }
    }

mybatis的事務(wù)對(duì)象為:

org.apache.ibatis.transaction.Transaction

Transaction是有mybatis的事務(wù)工廠產(chǎn)生的

org.apache.ibatis.transaction.TransactionFactory

而mybatis默認(rèn)的事務(wù)工廠為

org.mybatis.spring.transaction.SpringManagedTransactionFactory

見下面mybatis代碼

    package org.mybatis.spring;
    public class SqlSessionFactoryBean{
        protected SqlSessionFactory buildSqlSessionFactory(){
            //部分代碼略..........
            if (this.transactionFactory == null) {
                this.transactionFactory = new SpringManagedTransactionFactory();
            //部分代碼略.............
        }
        }
    }

SpringManagedTransactionFactory生產(chǎn)的事務(wù)的類為

org.mybatis.spring.transaction.SpringManagedTransaction

SpringManagedTransaction的getConnection會(huì)調(diào)用到openConnection函數(shù)毡代。

    private void openConnection() throws SQLException {
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.connection.getAutoCommit();
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

        if (logger.isDebugEnabled()) {
          logger.debug(
              "JDBC Connection ["
                  + this.connection
                  + "] will"
                  + (this.isConnectionTransactional ? " " : " not ")
                  + "be managed by Spring");
        }
    }

DataSourceUtils.getConnection最終會(huì)調(diào)用到DataSourceUtils.doGetConnection函數(shù)

    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");

        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(dataSource.getConnection());
            }
            return conHolder.getConnection();
        }
        // Else we either got no holder or an empty thread-bound holder here.

        logger.debug("Fetching JDBC Connection from DataSource");
        Connection con = dataSource.getConnection();

        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            logger.debug("Registering transaction synchronization for JDBC Connection");
            // Use same Connection for further JDBC actions within the transaction.
            // Thread-bound object will get removed by synchronization at transaction completion.
            ConnectionHolder holderToUse = conHolder;
            if (holderToUse == null) {
                holderToUse = new ConnectionHolder(con);
            }
            else {
                holderToUse.setConnection(con);
            }
            holderToUse.requested();
            TransactionSynchronizationManager.registerSynchronization(
                    new ConnectionSynchronization(holderToUse, dataSource));
            holderToUse.setSynchronizedWithTransaction(true);
            if (holderToUse != conHolder) {
                TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
            }
        }

        return con;
    }

上面的代碼首先會(huì)從線程上下文中獲取數(shù)據(jù)庫(kù)連接,如果不能獲取到勺疼,就直接從數(shù)據(jù)源中獲取教寂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市执庐,隨后出現(xiàn)的幾起案子酪耕,更是在濱河造成了極大的恐慌,老刑警劉巖轨淌,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迂烁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡递鹉,警方通過查閱死者的電腦和手機(jī)盟步,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)躏结,“玉大人却盘,你說我怎么就攤上這事∠彼” “怎么了黄橘?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)禀挫。 經(jīng)常有香客問我旬陡,道長(zhǎng),這世上最難降的妖魔是什么语婴? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任描孟,我火速辦了婚禮驶睦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘匿醒。我一直安慰自己场航,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布廉羔。 她就那樣靜靜地躺著溉痢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪憋他。 梳的紋絲不亂的頭發(fā)上孩饼,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音竹挡,去河邊找鬼镀娶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛揪罕,可吹牛的內(nèi)容都是我干的梯码。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼好啰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼轩娶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起框往,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鳄抒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后搅窿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘁酿,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年男应,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闹司。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沐飘,死狀恐怖游桩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耐朴,我是刑警寧澤借卧,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站筛峭,受9級(jí)特大地震影響铐刘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜影晓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一镰吵、第九天 我趴在偏房一處隱蔽的房頂上張望檩禾。 院中可真熱鬧,春花似錦疤祭、人聲如沸盼产。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)戏售。三九已至,卻和暖如春草穆,著一層夾襖步出監(jiān)牢的瞬間灌灾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工悲柱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留紧卒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓诗祸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親轴总。 傳聞我的和親對(duì)象是個(gè)殘疾皇子直颅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

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

  • 一、事務(wù)的基本原理Spring事務(wù)的本質(zhì)其實(shí)就是數(shù)據(jù)庫(kù)對(duì)事務(wù)的支持怀樟,沒有數(shù)據(jù)庫(kù)的事務(wù)支持功偿,spring是無(wú)法提供事...
    阿燈_supwinr閱讀 14,769評(píng)論 2 28
  • 一械荷、事務(wù)的基本原理 Spring事務(wù)的本質(zhì)其實(shí)就是數(shù)據(jù)庫(kù)對(duì)事務(wù)的支持,沒有數(shù)據(jù)庫(kù)的事務(wù)支持虑灰,spring是無(wú)法提供...
    芭蕾武閱讀 1,695評(píng)論 3 12
  • 深入理解 Spring 事務(wù)原理參考:https://mp.weixin.qq.com/s/HHLGfRuD_Yn...
    it_zzy閱讀 762評(píng)論 0 8
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,857評(píng)論 6 342
  • 月光 中秋佳節(jié) 月亮高懸 時(shí)光匆匆 光陰異世 一只銀狐叢樹珊中探出頭 感受人事情懷 一個(gè)夜行人 剛好從樹下路過 恰...
    WaldenWolf閱讀 144評(píng)論 6 5