myBatis源碼解析-數據源篇(3)

前言:咱們使用mybatis時,關於數據源的配置多使用如c3p0,druid等第三方的數據源。其實mybatis內置了數據源的實現,提供了鏈接數據庫,池的功能。在分析了緩存和日誌包的源碼後,接下來分析mybatis中的數據源實現。html

類圖:mybatis中關於數據源的源碼包路徑以下:java

 

 

 

mybatis中提供了一個DataSourceFactory接口,提供了設置數據源配置信息,獲取數據源方法。查看類圖可知,有三個實現類分別提供了不一樣的數據源實現。JndiDataSourceFactory,PooledDataSourceFactory,unPooledDataSourceFactory。JndiDataSourceFactory實現較簡單,此處源碼略過。以下爲各種的相互關係。sql

 

 

 

unPooledDataSourceFactory,PooledDataSourceFactory源碼分析:unPooledDataSourceFactory實現了DataSourceFactory接口,實現了數據源配置及獲取數據源方法。數據庫

// 對外提供的數據源工廠接口
public interface DataSourceFactory {
  // 設置配置信息
  void setProperties(Properties props);
 // 獲取數據源
  DataSource getDataSource();

}
// 非池化的數據源工廠類
public class UnpooledDataSourceFactory implements DataSourceFactory {
  
  private static final String DRIVER_PROPERTY_PREFIX = "driver."; // 數據庫驅動名前綴    
  private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length();

  protected DataSource dataSource; // 數據源    

  public UnpooledDataSourceFactory() { 
    this.dataSource = new UnpooledDataSource(); // 構造一個非池化的數據源(下文分析數據源詳細代碼)
  }

  public void setProperties(Properties properties) { // 對數據源進行配置,此處設計反射包的知識(本章重點不在這,可忽略)
    Properties driverProperties = new Properties();
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); // 將dataSource類轉爲metaObject類
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { // 如果數據庫驅動配置
        String value = properties.getProperty(propertyName);
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); // driverProperties存儲數據庫驅動參數
      } else if (metaDataSource.hasSetter(propertyName)) { // 若是有set方法
        String value = (String) properties.get(propertyName);
        // 根據屬性類型進行類型的轉換,主要是 Integer, Long, Boolean 三種類型的轉換
        Object convertedValue = convertValue(metaDataSource, propertyName, value); 
        // 設置DataSource 的相關屬性值
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    // 設置 DataSource.driverProerties 屬性值
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }
  // 獲取數據源
  public DataSource getDataSource() {
    return dataSource;
  }
 // 對Integer, Long, Boolean 三種類型的轉換
  private Object convertValue(MetaObject metaDataSource, String propertyName, String value) {
    Object convertedValue = value;
    Class<?> targetType = metaDataSource.getSetterType(propertyName);
    if (targetType == Integer.class || targetType == int.class) {
      convertedValue = Integer.valueOf(value);
    } else if (targetType == Long.class || targetType == long.class) {
      convertedValue = Long.valueOf(value);
    } else if (targetType == Boolean.class || targetType == boolean.class) {
      convertedValue = Boolean.valueOf(value);
    }
    return convertedValue;
  }

}
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

  public PooledDataSourceFactory() {
    // dataSource實現類變爲PooledDataSource
    this.dataSource = new PooledDataSource();
  }

}

unPooledDataSourceFactory主要工做是對數據源進行參數配置,並提供獲取數據源方法。分析PooledDataSourceFactory源碼,只是繼承unPooledDataSourceFactory,將DataSource實現類改變爲PooledDataSource。編程

unPooledDataSource源碼分析基本的數據源實現都實現了DataSource接口,重寫獲取數據庫鏈接的方法。unPooledDataSource從類名可知,不支持數據庫鏈接的池化。也就是說,每來一個獲取鏈接請求,就新建一個數據庫鏈接。讓咱們看源碼驗證下。設計模式

public class UnpooledDataSource implements DataSource {
  
  private ClassLoader driverClassLoader; // 數據庫驅動類加載器
  private Properties driverProperties; // 有關數據庫驅動的參數
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>(); // 緩存已註冊過的數據庫驅動

  private String driver; // 數據庫驅動
  private String url; // 數據庫名
  private String username; // 鏈接用戶名
  private String password; // 密碼

  private Boolean autoCommit; // 是否自動提交
  private Integer defaultTransactionIsolationLevel; // 事物隔離級別

  static { // 初始化
    Enumeration<Driver> drivers = DriverManager.getDrivers();  // DriverManager中已存在的數據庫驅動加載到數據庫驅動緩存
    while (drivers.hasMoreElements()) {
      Driver driver = drivers.nextElement();
      registeredDrivers.put(driver.getClass().getName(), driver);
    }
  }
  .....
  
  
  public Connection getConnection() throws SQLException {
    return doGetConnection(username, password);
  }
  
  // 獲取數據庫鏈接
  private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver(); // 初始化數據庫驅動
    Connection connection = DriverManager.getConnection(url, properties); // 此處每次獲取鏈接,就新建一個數據庫鏈接
    configureConnection(connection); // 設置數據庫是否自動提交,設置數據庫事物隔離級別
    return connection;
  }

  private synchronized void initializeDriver() throws SQLException {
    // 若此驅動還沒初始化,則進行初始化
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver)driverType.newInstance();
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

  private void configureConnection(Connection conn) throws SQLException {
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  } 
  ....
}

 以上代碼是UnPooledDataSource的源碼分析,可見,UnPooledDataSource並無採用池化的方法對數據庫鏈接進行管理。每次獲取鏈接,就新建一個數據庫鏈接。咱們知道數據庫鏈接的創建是個很是耗時耗資源的過程,爲了統一管理這些數據庫鏈接,mybatis爲咱們引入了PooledDataSource類。緩存

PooledDataSource源碼分析:PooledDataSource是數據源的重點,源碼比較複雜。PooledDataSource內部使用UnPooledDataSource類建立新的數據庫鏈接。PooledDataSource並不直接管理java.sql.connection鏈接,而是管理java.sql.connection的一個代理類PooledConnection。除了管理數據庫鏈接的創建,PooledDataSource內部還使用PoolState來管理數據源的狀態(即空閒鏈接數,活躍鏈接數等)。綜上,總結以下,PooledDataSource使用UnPooledDataSource類爲數據源建立真實的數據庫鏈接,使用PooledConnection爲數據源管理數據庫鏈接,使用PoolState來爲數據源管理數據源當前狀態。mybatis

 

 

 PoolConnection是一個connection代理類,裏面封裝了真實的鏈接與代理鏈接,如今咱們先來分析PoolConnection的源碼。源碼分析

class PooledConnection implements InvocationHandler {  // 鏈接代理類

  private static final String CLOSE = "close";
  private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };

  private int hashCode = 0;
  private PooledDataSource dataSource; // 數據源
  private Connection realConnection; // 被代理的真實鏈接
  private Connection proxyConnection; // 代理鏈接
  private long checkoutTimestamp; // 從鏈接池中取出鏈接的時間
  private long createdTimestamp; // 鏈接創建的時間
  private long lastUsedTimestamp; // 鏈接上次使用的時間
  private int connectionTypeCode; // 用於標註該鏈接所在的鏈接池
  private boolean valid; // 鏈接有效的標誌

PooledConnection實現了InvocationHandler接口,則可見是一個代理對象。查看屬性可知,內部有真實鏈接與代理鏈接,並附帶鏈接的一些記錄信息。查看該類的構造方法。學習

public PooledConnection(Connection connection, PooledDataSource dataSource) {
    this.hashCode = connection.hashCode();
    this.realConnection = connection;
    this.dataSource = dataSource;
    this.createdTimestamp = System.currentTimeMillis();
    this.lastUsedTimestamp = System.currentTimeMillis();
    this.valid = true; // 該連接是否有效
    this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);  // 使用動態代理生成鏈接的代理類
  }

  /*
   * Invalidates the connection
   */
  // 將該連接置爲無效    
  public void invalidate() {
    valid = false;
  }

查看構造方法可知,內部除了初始化一些屬性外,還將鏈接的代理類也進行初始化了。那代理類究竟作了什麼,查看重寫的invoke方法源碼。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  // 代理方法
    String methodName = method.getName(); // 獲取方法名
    if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { // 如果close方法,則將該鏈接放入數據源中
      dataSource.pushConnection(this);
      return null;
    } else {
      try {
        if (!Object.class.equals(method.getDeclaringClass())) {  // 若要執行的方法不是object方法,則檢查鏈接的有效性
          // issue #579 toString() should never fail
          // throw an SQLException instead of a Runtime
          checkConnection();
        }
        return method.invoke(realConnection, args);  //執行真實的方法
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  }

  private void checkConnection() throws SQLException {
    if (!valid) {
      throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
    }
  }

由源碼可知,代理鏈接在執行方法時,會先檢查此鏈接的有效性,而後執行真實的方法。分析完PoolConnection後,對PoolState進行源碼解析。

public class PoolState {  // 鏈接池狀態信息

  protected PooledDataSource dataSource; // 此狀態信息關聯的數據源

  protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>(); // 空閒鏈接列表
  protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>(); // 活躍鏈接列表
  protected long requestCount = 0; // 請求數
  protected long accumulatedRequestTime = 0; // 累加請求所用時間
  protected long accumulatedCheckoutTime = 0; // 累加佔用鏈接所用時間
  protected long claimedOverdueConnectionCount = 0;  // 鏈接超時的數量
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 累加超時的鏈接超時的時間
  protected long accumulatedWaitTime = 0; // 累加等待獲取鏈接所用時間
  protected long hadToWaitCount = 0; // 等待獲取鏈接的線程數
  protected long badConnectionCount = 0; // 失效的鏈接數

PoolState是對DataSource的狀態管理類,主要包括如累計鏈接超時時間,失效鏈接的獲取等一些狀態信息的管理。除了包括一些數據庫鏈接的記錄信息外,內部還維護了兩個數據庫鏈接的列表idleConnections,activeConnections.。分別用來存放空閒的數據庫鏈接列表,活躍的數據庫鏈接列表,針對此兩個列表的操做,下文在分析PooledDataSource時會進行詳細介紹。

對PoolConnection和PoolState分析結束後,具體分析PoolDataSource源碼。

public class PooledDataSource implements DataSource {

  private static final Log log = LogFactory.getLog(PooledDataSource.class);

  private final PoolState state = new PoolState(this); // 維護數據源的狀態

  private final UnpooledDataSource dataSource; // 使用UnpooledDataSource來創建真正的鏈接

  // OPTIONAL CONFIGURATION FIELDS
  protected int poolMaximumActiveConnections = 10; // 最大活躍的鏈接數
  protected int poolMaximumIdleConnections = 5;  // 最大空閒的鏈接數
  protected int poolMaximumCheckoutTime = 20000; // 最大checkout時間(checkOutTime指的是從數據源中獲取鏈接到歸還鏈接的時間)
  protected int poolTimeToWait = 20000; // 最大等待時間
  protected String poolPingQuery = "NO PING QUERY SET"; // 使用該語句來驗證該鏈接是否有效
  protected boolean poolPingEnabled = false;
  protected int poolPingConnectionsNotUsedFor = 0;

  private int expectedConnectionTypeCode; // hashcode

查看PoolDataSource基本屬性,可知內部使用PoolState來維護數據源的狀態信息,使用UnpooledDataSource來產真正的鏈接。並提供了一些如設置最大空閒,活躍鏈接數的配置信息。做爲DataSource的實現,PooledDataSource不只提供瞭如popConnection獲取數據庫鏈接的接口。還提供了forceCloseAll來關閉全部數據鏈接。pushConnection將使用結束的數據庫鏈接放入數據源中。如今開始分析第一個方法popConnection,流程圖以下,代碼中都有詳細註釋,請耐看。

 

 

 

 // 獲取鏈接
  private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
    PooledConnection conn = null;
    long t = System.currentTimeMillis(); // 開始取鏈接時間
    int localBadConnectionCount = 0; // 壞的鏈接數
    // 鏈接未取到,一直循環
    while (conn == null) {
      synchronized (state) { // 加鎖
        if (state.idleConnections.size() > 0) {  // 鏈接池中是否有空閒鏈接
          // Pool has available connection
          conn = state.idleConnections.remove(0); // 從空閒鏈接列表中取一個空閒鏈接
          if (log.isDebugEnabled()) {
            log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
          }
        } else { // 鏈接池無空閒鏈接
          // Pool does not have available connection
          if (state.activeConnections.size() < poolMaximumActiveConnections) { // 當前活躍鏈接數小於鏈接池的最大活躍鏈接數
            // Can create new connection
            conn = new PooledConnection(dataSource.getConnection(), this); // 則使用unPooledDataSource新建一個鏈接,並封裝成代理鏈接PooledConnection
            @SuppressWarnings("unused")
            //used in logging, if enabled
            Connection realConn = conn.getRealConnection(); // 獲取真正的鏈接
            if (log.isDebugEnabled()) {
              log.debug("Created connection " + conn.getRealHashCode() + ".");
            }
          } else { // 不然當前活躍鏈接數大於等於鏈接池的最大活躍鏈接數
            // Cannot create new connection
            PooledConnection oldestActiveConnection = state.activeConnections.get(0); // 從活躍鏈接列表中取第一個活躍鏈接
            long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); // 獲取鏈接已經獲取了多長時間
            if (longestCheckoutTime > poolMaximumCheckoutTime) {  // 檢測該鏈接是否超時
              // Can claim overdue connection  超時則進行統計
              state.claimedOverdueConnectionCount++;
              state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
              state.accumulatedCheckoutTime += longestCheckoutTime;
              state.activeConnections.remove(oldestActiveConnection);  // 將此超時鏈接從活躍鏈接列表中移除
              if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                // 超時且關閉了自動提交,則進行回滾
                oldestActiveConnection.getRealConnection().rollback();
              }
              conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); // 新建一個代理鏈接
              oldestActiveConnection.invalidate();   // 將超時的鏈接設置爲失效
              if (log.isDebugEnabled()) {
                log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
              }
            } else {  // 若沒有一個鏈接超時,則必須等待
              // Must wait
              try {
                if (!countedWait) {
                  state.hadToWaitCount++; // 等待數加一
                  countedWait = true;
                }
                if (log.isDebugEnabled()) {
                  log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                }
                long wt = System.currentTimeMillis();
                state.wait(poolTimeToWait); // 線程等待
                state.accumulatedWaitTime += System.currentTimeMillis() - wt; // 計算累積等待時間
              } catch (InterruptedException e) { // 線程阻塞中斷
                break;
              }
            }
          }
        }
        if (conn != null) {
          if (conn.isValid()) { // 若鏈接有效
            if (!conn.getRealConnection().getAutoCommit()) {
              // 回滾操做
              conn.getRealConnection().rollback();
            }
            conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
            conn.setCheckoutTimestamp(System.currentTimeMillis()); // 設置取用鏈接的時間戳
            conn.setLastUsedTimestamp(System.currentTimeMillis()); // 設置最後一次使用鏈接的時間戳
            state.activeConnections.add(conn); // 添加到活躍鏈接列表
            state.requestCount++; // 請求加一
            state.accumulatedRequestTime += System.currentTimeMillis() - t; // 累計創建鏈接所用時間
          } else {  // 若此鏈接失效
            if (log.isDebugEnabled()) {
              log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
            }
            state.badConnectionCount++; // 記錄壞的鏈接數+1
            localBadConnectionCount++;
            conn = null; // 置爲空,開始下一次循環
            if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
              if (log.isDebugEnabled()) {
                log.debug("PooledDataSource: Could not get a good connection to the database.");
              }
              throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
            }
          }
        }
      }

    }
    // 返回鏈接
    if (conn == null) {
      if (log.isDebugEnabled()) {
        log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
      }
      throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }

    return conn;
  }

經分析,獲取鏈接的過程爲,先去查找空閒鏈接列表,若存在空閒列表,則直接從空閒列表中拿出數據庫鏈接。若無空閒鏈接,則判斷當前存活的數據庫鏈接是否超過了指定的活躍鏈接數,若沒有超過,則新建數據庫鏈接。若超過了,則去拿活躍鏈接數的第一個鏈接判斷是否鏈接超時(爲何拿第一個?由於是隊列,隊尾插入,對頭獲取,對頭的鏈接沒有超時,則後面的確定沒有超時)若發現第一個鏈接已經超過指定的數據庫鏈接時間,則將此鏈接從活躍列表中移除,並標誌爲失效,而後本身新建一個數據庫鏈接。若第一個鏈接沒有過時,則表明如今數據源不能提供任何鏈接了,必須等待,直接wait,釋放鎖,等待線程喚醒。拿到了數據庫鏈接後,須要檢查該鏈接是否有效,如有效,則放入活躍鏈接列表中,並返回給用戶。

當一個鏈接使用完畢後,須要放回到數據源中進行管理,如今分析pushConnection源碼。流程圖和源碼分析以下:

 

 

protected void pushConnection(PooledConnection conn) throws SQLException {

    synchronized (state) {
      state.activeConnections.remove(conn); // 將此鏈接從活躍鏈接列表中移除
      if (conn.isValid()) { // 鏈接有效
        if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { // 沒有超過最大空閒鏈接數且是同一個鏈接池
          state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 累加鏈接時間
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); // 新建一個代理鏈接
          state.idleConnections.add(newConn); // 添加到空閒鏈接列表中
          newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
          newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
          conn.invalidate(); // 將老的代理鏈接置爲不可用
          if (log.isDebugEnabled()) {
            log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
          }
          state.notifyAll(); // 喚醒阻塞的鏈接
        } else { // 已達到最大空閒鏈接
          state.accumulatedCheckoutTime += conn.getCheckoutTime();
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          conn.getRealConnection().close(); // 直接關閉
          if (log.isDebugEnabled()) {
            log.debug("Closed connection " + conn.getRealHashCode() + ".");
          }
          conn.invalidate(); // 將代理鏈接置爲不可用
        }
      } else {
        if (log.isDebugEnabled()) {
          log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
        }
        state.badConnectionCount++;
      }
    }
  }

鏈接使用結束後並非立馬釋放,而是檢查當前空閒列表的鏈接數是否已超過指定空閒的鏈接數,若沒有超過,則放入到空閒鏈接列表中。不然將該鏈接設爲無效。並喚醒阻塞中的獲取鏈接的線程。

當用戶指定變動數據源配置信息時,如數據庫地址,用戶名,密碼等,都須要對數據源進行重置,清空現存的數據庫鏈接後修改配置信息。現查看清空數據源的方法forceCloseAll源碼。此方法較簡單,就不貼流程圖了。

//關閉池中全部的活躍鏈接和空閒鏈接 
  public void forceCloseAll() {
    synchronized (state) {
      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
      for (int i = state.activeConnections.size(); i > 0; i--) { // 獲取全部的活躍鏈接
        try {
          PooledConnection conn = state.activeConnections.remove(i - 1); // 移除
          conn.invalidate(); // 失效

          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) { // 事務回滾
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
      for (int i = state.idleConnections.size(); i > 0; i--) { // 獲取全部的空閒鏈接
        try {
          PooledConnection conn = state.idleConnections.remove(i - 1); // 移除
          conn.invalidate(); // 失效

          Connection realConn = conn.getRealConnection();
          if (!realConn.getAutoCommit()) { // 事務回滾
            realConn.rollback();
          }
          realConn.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
    if (log.isDebugEnabled()) {
      log.debug("PooledDataSource forcefully closed/removed all connections.");
    }
  }

經分析,forceCloseAll對全部的空閒列表中,活躍列表中的數據庫鏈接所有移除並置爲不可用。池中恢復到初始化狀態。

總結:本文對mybatis中的數據源部分進行了源碼解析。在學習源碼的過程當中,加深了對不少設計模式的理解,體會到了大神們的編程習慣,不只僅是源碼自己,更多的是思想上的理解。在學習中也知道了不急於求成,一個一個的包去分析,而後再去整和業務流程。如你對此源碼也感興趣,能夠評論下,我會把本身的mybatis中文註釋源碼包分享。但此註釋都是本身手寫,不能確保準確性,僅提供參考。任重而道遠,源碼之路但願本身能堅持下來。

相關文章
相關標籤/搜索