溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java數(shù)據(jù)庫連接池之DBCP淺析_動力節(jié)點Java學院整理

發(fā)布時間:2020-10-13 13:11:40 來源:腳本之家 閱讀:184 作者:wang-meng 欄目:編程語言

一. 為何要使用數(shù)據(jù)庫連接池

假設網站一天有很大的訪問量,數(shù)據(jù)庫服務器就需要為每次連接創(chuàng)建一次數(shù)據(jù)庫連接,極大的浪費數(shù)據(jù)庫的資源,并且極易造成數(shù)據(jù)庫服務器內存溢出、拓機。

數(shù)據(jù)庫連接是一種關鍵的有限的昂貴的資源,這一點在多用戶的網頁應用程序中體現(xiàn)的尤為突出.對數(shù)據(jù)庫連接的管理能顯著影響到整個應用程序的伸縮性和健壯性,影響到程序的性能指標.數(shù)據(jù)庫連接池正式針對這個問題提出來的.數(shù)據(jù)庫連接池負責分配,管理和釋放數(shù)據(jù)庫連接,它允許應用程序重復使用一個現(xiàn)有的數(shù)據(jù)庫連接,而不是重新建立一個。

數(shù)據(jù)庫連接池在初始化時將創(chuàng)建一定數(shù)量的數(shù)據(jù)庫連接放到連接池中, 這些數(shù)據(jù)庫連接的數(shù)量是由最小數(shù)據(jù)庫連接數(shù)來設定的.無論這些數(shù)據(jù)庫連接是否被使用,連接池都將一直保證至少擁有這么多的連接數(shù)量.連接池的最大數(shù)據(jù)庫連接數(shù)量限定了這個連接池能占有的最大連接數(shù),當應用程序向連接池請求的連接數(shù)超過最大連接數(shù)量時,這些請求將被加入到等待隊列中.

數(shù)據(jù)庫連接池的最小連接數(shù)和最大連接數(shù)的設置要考慮到以下幾個因素:

  1, 最小連接數(shù):是連接池一直保持的數(shù)據(jù)庫連接,所以如果應用程序對數(shù)據(jù)庫連接的使用量不大,將會有大量的數(shù)據(jù)庫連接資源被浪費.
  2, 最大連接數(shù):是連接池能申請的最大連接數(shù),如果數(shù)據(jù)庫連接請求超過次數(shù),后面的數(shù)據(jù)庫連接請求將被加入到等待隊列中,這會影響以后的數(shù)據(jù)庫操作
  3, 如果最小連接數(shù)與最大連接數(shù)相差很大:那么最先連接請求將會獲利,之后超過最小連接數(shù)量的連接請求等價于建立一個新的數(shù)據(jù)庫連接.不過,這些大于最小連接數(shù)的數(shù)據(jù)庫連接在使用完不會馬上被釋放,他將被           放到連接池中等待重復使用或是空間超時后被釋放.

二, 數(shù)據(jù)庫連接池的原理及實現(xiàn)

到了這里我們已經知道數(shù)據(jù)庫連接池是用來做什么的了, 下面我們就來說數(shù)據(jù)庫連接池是如何來實現(xiàn)的.
1, 建立一個數(shù)據(jù)庫連接池pool, 池中有若干個Connection 對象, 當用戶發(fā)來請求需要進行數(shù)據(jù)庫交互時則會使用池中第一個Connection對象.
2, 當本次連接結束時, 再將這個Connection對象歸還池中, 這樣就可以保證池中一直有足夠的Connection對象.

public class SimplePoolDemo {
 //創(chuàng)建一個連接池
 private static LinkedList<Connection> pool = new LinkedList<Connection>(); 
 
 //初始化10個連接
 static{
  try {
   for (int i = 0; i < 10; i++) {
    Connection conn = DBUtils.getConnection();//得到一個連接
    pool.add(conn);
   }
  } catch (Exception e) {
   throw new ExceptionInInitializerError("數(shù)據(jù)庫連接失敗,請檢查配置");
  }
 }
 //從池中獲取一個連接
 public static Connection getConnectionFromPool(){
  return pool.removeFirst();//移除一個連接對象
 }
 //釋放資源
 public static void release(Connection conn){
  pool.addLast(conn);
 }
}

以上的Demo就是一個簡單的數(shù)據(jù)庫連接池的例子, 先在靜態(tài)代碼塊中初始化10個Connection對象, 當本次請求結束后再將Connection添加進池中.

這只是我們自己手動去實現(xiàn)的, 當然在實際生產中并不需要我們去手動去寫數(shù)據(jù)庫連接池. 下面就重點講DBCP和C3P0的實現(xiàn)方式.

三, DBCP連接池

首先我們來看DBCP 的例子, 然后根據(jù)例子來分析:

#連接設置
 driverClassName=com.mysql.jdbc.Driver
 url=jdbc:mysql://localhost:3306/bjpowernode
 username=root
 password=abc
 
 #<!-- 初始化連接 -->
 initialSize=10
 
#最大連接數(shù)量
maxActive=50
 
#<!-- 最大空閑連接 -->
maxIdle=20

#<!-- 最小空閑連接 -->
minIdle=5
 
#<!-- 超時等待時間以毫秒為單位 60000毫秒/1000等于60秒 -->
maxWait=60000

 
#JDBC驅動建立連接時附帶的連接屬性屬性的格式必須為這樣:[屬性名=property;] 
#注意:"user" 與 "password" 兩個屬性會被明確地傳遞,因此這里不需要包含他們。
connectionProperties=useUnicode=true;characterEncoding=utf8
 
#指定由連接池所創(chuàng)建的連接的自動提交(auto-commit)狀態(tài)。
defaultAutoCommit=true

#driver default 指定由連接池所創(chuàng)建的連接的只讀(read-only)狀態(tài)。
#如果沒有設置該值,則“setReadOnly”方法將不被調用。(某些驅動并不支持只讀模式,如:Informix)
defaultReadOnly=
 
#driver default 指定由連接池所創(chuàng)建的連接的事務級別(TransactionIsolation)。
#可用值為下列之一:(詳情可見javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
defaultTransactionIsolation=REPEATABLE_READ

DBCP配置文件
DBCPUtils:

public class DBCPUtils {
 private static DataSource ds;//定義一個連接池對象
 static{
  try {
   Properties pro = new Properties();
   pro.load(DBCPUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties"));
   ds = BasicDataSourceFactory.createDataSource(pro);//得到一個連接池對象
  } catch (Exception e) {
   throw new ExceptionInInitializerError("初始化連接錯誤,請檢查配置文件!");
  }
 }
 //從池中獲取一個連接
 public static Connection getConnection() throws SQLException{
  return ds.getConnection();
 }
 
 public static void closeAll(ResultSet rs,Statement stmt,Connection conn){
  if(rs!=null){
   try {
    rs.close();
   } catch (SQLException e) {
    e.printStackTrace();
   }
  }
  
  if(stmt!=null){
   try {
    stmt.close();
   } catch (SQLException e) {
    e.printStackTrace();
   }
  }
  
  if(conn!=null){
   try {
    conn.close();//關閉
   } catch (SQLException e) {
    e.printStackTrace();
   }
  }
 }
}

在這個closeAll方法中, conn.close(); 這個地方會將connection還回到池子中嗎? DataSource 中是如何處理close()方法的呢?

上面的兩個問題就讓我們一起來看看源碼是如何來實現(xiàn)的吧.

這里我們從ds.getConnection();入手, 看看一個數(shù)據(jù)源DataSource是如何創(chuàng)建connection的.
用eclipse導入:commons-dbcp-1.4-src.zip和commons-pool-1.5.6-src.zip則可查看源碼:

BasicDataSource.class:(implements DataSource)

public Connection getConnection() throws SQLException {
  return createDataSource().getConnection();
}

接下來看createDataSoruce() 方法:

protected synchronized DataSource createDataSource()
 throws SQLException {
 if (closed) {
  throw new SQLException("Data source is closed");
 }

 // Return the pool if we have already created it
 if (dataSource != null) {
  return (dataSource);
 }

 // create factory which returns raw physical connections
 ConnectionFactory driverConnectionFactory = createConnectionFactory();

 // create a pool for our connections
 createConnectionPool();

 // Set up statement pool, if desired
 GenericKeyedObjectPoolFactory statementPoolFactory = null;
 if (isPoolPreparedStatements()) {
  statementPoolFactory = new GenericKeyedObjectPoolFactory(null,
     -1, // unlimited maxActive (per key)
     GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL,
     0, // maxWait
     1, // maxIdle (per key)
     maxOpenPreparedStatements);
 }

 // Set up the poolable connection factory
 createPoolableConnectionFactory(driverConnectionFactory, statementPoolFactory, abandonedConfig);

 // Create and return the pooling data source to manage the connections
 createDataSourceInstance();
 
 try {
  for (int i = 0 ; i < initialSize ; i++) {
   connectionPool.addObject();
  }
 } catch (Exception e) {
  throw new SQLNestedException("Error preloading the connection pool", e);
 }
 
 return dataSource;
}

從源代碼可以看出,createDataSource()方法通過7步,逐步構造出一個數(shù)據(jù)源,下面是詳細的步驟:

   1、檢查數(shù)據(jù)源是否關閉或者是否創(chuàng)建完成,如果關閉了就拋異常,如果已經創(chuàng)建完成就直接返回。

   2、調用createConnectionFactory()創(chuàng)建JDBC連接工廠driverConnectionFactory,這個工廠使用數(shù)據(jù)庫驅動來創(chuàng)建最底層的JDBC連接

   3、調用createConnectionPool()創(chuàng)建數(shù)據(jù)源使用的連接池,連接池顧名思義就是緩存JDBC連接的地方。

   4、如果需要就設置statement的緩存池,這個一般不需要設置

   5、調用createPoolableConnectionFactory創(chuàng)建PoolableConnection的工廠,這個工廠使用上述driverConnectionFactory來創(chuàng)建底層JDBC連接,然后包裝出一個PoolableConnection,這個PoolableConnection與連接池設置了一對多的關系,也就是說,連接池中存在多個PoolableConnection,每個PoolableConnection都關聯(lián)同一個連接池,這樣的好處是便于該表PoolableConnection的close方法的行為,具體會在后面詳細分析。

   6、調用createDataSourceInstance()創(chuàng)建內部數(shù)據(jù)源

   7、為連接池中添加PoolableConnection

經過以上7步,一個數(shù)據(jù)源就形成了,這里明確一點,一個數(shù)據(jù)源本質就是連接池+連接+管理策略。下面,將對每一步做詳細的分析。

JDBC連接工廠driverConnectionFactory的創(chuàng)建過程

protected ConnectionFactory createConnectionFactory() throws SQLException {
 // Load the JDBC driver class
 Class driverFromCCL = null;
 if (driverClassName != null) {
  try {
   try {
    if (driverClassLoader == null) {
     Class.forName(driverClassName);
    } else {
     Class.forName(driverClassName, true, driverClassLoader);
    }
   } catch (ClassNotFoundException cnfe) {
    driverFromCCL = Thread.currentThread(
      ).getContextClassLoader().loadClass(
        driverClassName);
   }
  } catch (Throwable t) {
   String message = "Cannot load JDBC driver class '" +
    driverClassName + "'";
   logWriter.println(message);
   t.printStackTrace(logWriter);
   throw new SQLNestedException(message, t);
  }
 }

 // Create a JDBC driver instance
 Driver driver = null;
 try {
  if (driverFromCCL == null) {
   driver = DriverManager.getDriver(url);
  } else {
   // Usage of DriverManager is not possible, as it does not
   // respect the ContextClassLoader
   driver = (Driver) driverFromCCL.newInstance();
   if (!driver.acceptsURL(url)) {
    throw new SQLException("No suitable driver", "08001"); 
   }
  }
 } catch (Throwable t) {
  String message = "Cannot create JDBC driver of class '" +
   (driverClassName != null ? driverClassName : "") +
   "' for connect URL '" + url + "'";
  logWriter.println(message);
  t.printStackTrace(logWriter);
  throw new SQLNestedException(message, t);
 }

 // Can't test without a validationQuery
 if (validationQuery == null) {
  setTestOnBorrow(false);
  setTestOnReturn(false);
  setTestWhileIdle(false);
 }

 // Set up the driver connection factory we will use
 String user = username;
 if (user != null) {
  connectionProperties.put("user", user);
 } else {
  log("DBCP DataSource configured without a 'username'");
 }

 String pwd = password;
 if (pwd != null) {
  connectionProperties.put("password", pwd);
 } else {
  log("DBCP DataSource configured without a 'password'");
 }

 ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driver, url, connectionProperties);
 return driverConnectionFactory;
}

上面一連串代碼干了什么呢?其實就干了兩件事:

1、獲取數(shù)據(jù)庫驅動

2、使用驅動以及參數(shù)(url、username、password)構造一個工廠。

一旦這個工廠構建完畢了,就可以來生成連接,而這個連接的生成其實是驅動加上配置來完成的.

創(chuàng)建連接池的過程

protected void createConnectionPool() {
  // Create an object pool to contain our active connections
  GenericObjectPool gop;
  if ((abandonedConfig != null) && (abandonedConfig.getRemoveAbandoned())) {
   gop = new AbandonedObjectPool(null,abandonedConfig);
  }
  else {
   gop = new GenericObjectPool();
  }
  gop.setMaxActive(maxActive);
  gop.setMaxIdle(maxIdle);
  gop.setMinIdle(minIdle);
  gop.setMaxWait(maxWait);
  gop.setTestOnBorrow(testOnBorrow);
  gop.setTestOnReturn(testOnReturn);
  gop.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
  gop.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
  gop.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
  gop.setTestWhileIdle(testWhileIdle);
  connectionPool = gop;
 }

在創(chuàng)建連接池的時候,用到了common-pool里的GenericObjectPool,對于JDBC連接的緩存以及管理其實是交給GenericObjectPool的,DBCP其實只是負責創(chuàng)建這樣一種pool然后使用它而已。

 創(chuàng)建statement緩存池
一般來說,statement并不是重量級的對象,創(chuàng)建過程消耗的資源并不像JDBC連接那樣重,所以沒必要做緩存池化,這里為了簡便起見,對此不做分析。

創(chuàng)建PoolableConnectionFactory

這一步是一個承上啟下的過程,承上在于利用上面兩部創(chuàng)建的連接工廠和連接池,構建PoolableConnectionFactory,啟下則在于為后面的向連接池里添加連接做準備。

下面先上一張靜態(tài)的類關系圖:

protected void createPoolableConnectionFactory(ConnectionFactory driverConnectionFactory,
  KeyedObjectPoolFactory statementPoolFactory, AbandonedConfig configuration) throws SQLException {
 PoolableConnectionFactory connectionFactory = null;
 try {
  connectionFactory =
   new PoolableConnectionFactory(driverConnectionFactory,
           connectionPool,
           statementPoolFactory,
           validationQuery,
           validationQueryTimeout,
           connectionInitSqls,
           defaultReadOnly,
           defaultAutoCommit,
           defaultTransactionIsolation,
           defaultCatalog,
           configuration);
  validateConnectionFactory(connectionFactory);
 } catch (RuntimeException e) {
  throw e;
 } catch (Exception e) {
  throw new SQLNestedException("Cannot create PoolableConnectionFactory (" + e.getMessage() + ")", e);
 }
}

可以看見,在創(chuàng)建PoolableConnectionFactory的時候,需要用到前面創(chuàng)建的driverConnectionFactory以及連接池connectionPool,那么那個構造函數(shù)到底干了先什么呢?

public PoolableConnectionFactory(
 ConnectionFactory connFactory,
 ObjectPool pool,
 KeyedObjectPoolFactory stmtPoolFactory,
 String validationQuery,
 int validationQueryTimeout,
 Collection connectionInitSqls,
 Boolean defaultReadOnly,
 boolean defaultAutoCommit,
 int defaultTransactionIsolation,
 String defaultCatalog,
 AbandonedConfig config) {

 _connFactory = connFactory;
 _pool = pool;
 _config = config;
 _pool.setFactory(this);
 _stmtPoolFactory = stmtPoolFactory;
 _validationQuery = validationQuery;
 _validationQueryTimeout = validationQueryTimeout;
 _connectionInitSqls = connectionInitSqls;
 _defaultReadOnly = defaultReadOnly;
 _defaultAutoCommit = defaultAutoCommit;
 _defaultTransactionIsolation = defaultTransactionIsolation;
 _defaultCatalog = defaultCatalog;
}

 它在內部保存了真正的JDBC 連接的工廠以及連接池,然后,通過一句_pool.setFactory(this); 將它自己設置給了連接池。這行代碼十分重要,要理解這行代碼,首先需要明白common-pool中的GenericObjectPool添加內部元素的一般方法,沒錯,那就是必須要傳入一個工廠Factory。GenericObjectPool添加內部元素時會調用addObject()這個方法,內部其實是調用工廠的makeObejct()方法來創(chuàng)建元素,然后再加入到自己的池中。_pool.setFactory(this)這句代碼其實起到了啟下的作用,沒有它,后面的為連接池添加連接也就不可能完成。

   當創(chuàng)建完工廠后,會有個validateConnectionFactory(connectionFactory);這個方法的作用僅僅是用來驗證數(shù)據(jù)庫連接可使用,看代碼:

protected static void validateConnectionFactory(PoolableConnectionFactory connectionFactory) throws Exception {
 Connection conn = null;
 try {
  conn = (Connection) connectionFactory.makeObject();
  connectionFactory.activateObject(conn);
  connectionFactory.validateConnection(conn);
  connectionFactory.passivateObject(conn);
 }
 finally {
  connectionFactory.destroyObject(conn);
 }
}

先是用makeObject方法來創(chuàng)建一個連接,然后做相關驗證(就是用一些初始化sql來試著執(zhí)行一下,看看能不能連接到數(shù)據(jù)庫),然后銷毀連接,這里并沒有向連接池添加連接,真正的添加連接在后面,不過,我們可以先通過下面一張時序圖來看看makeObject方法到底做了什么。

下面是一張整體流程的時序圖:

從圖中可以看出,makeObject方法的大致流程:從driverConnectionFactory那里拿到底層連接,初始化驗證,然后創(chuàng)建PoolableConnection,在創(chuàng)建這個PoolableConnection的時候,將PoolableConnection與連接池關聯(lián)了起來,真正做到了連接池和連接之間的一對多的關系,這也為改變PoolableConnection的close方法提供了方便。

下面是makeObject方法的源代碼:

public Object makeObject() throws Exception {
 Connection conn = _connFactory.createConnection();
 if (conn == null) {
  throw new IllegalStateException("Connection factory returned null from createConnection");
 }
 initializeConnection(conn); //初始化,這個過程可有可無
 if(null != _stmtPoolFactory) { 
  KeyedObjectPool stmtpool = _stmtPoolFactory.createPool();
  conn = new PoolingConnection(conn,stmtpool);
  stmtpool.setFactory((PoolingConnection)conn);
 }
 //這里是關鍵
 return new PoolableConnection(conn,_pool,_config); 
}

其中PoolableConnection的構造函數(shù)如下:

public PoolableConnection(Connection conn, ObjectPool pool, AbandonedConfig config) {
 super(conn, config);
 _pool = pool;
}

內部關聯(lián)了一個連接池,這個連接池的作用體現(xiàn)在PoolableConnection的close方法中:

public synchronized void close() throws SQLException {
 if (_closed) {
  // already closed
  return;
 }

 boolean isUnderlyingConectionClosed;
 try {
  isUnderlyingConectionClosed = _conn.isClosed();
 } catch (SQLException e) {
  try {
   _pool.invalidateObject(this); // XXX should be guarded to happen at most once
  } catch(IllegalStateException ise) {
   // pool is closed, so close the connection
   passivate();
   getInnermostDelegate().close();
  } catch (Exception ie) {
   // DO NOTHING the original exception will be rethrown
  }
  throw (SQLException) new SQLException("Cannot close connection (isClosed check failed)").initCause(e);
 }

 if (!isUnderlyingConectionClosed) {
  // Normal close: underlying connection is still open, so we
  // simply need to return this proxy to the pool
  try {
   _pool.returnObject(this); // XXX should be guarded to happen at most once
  } catch(IllegalStateException e) {
   // pool is closed, so close the connection
   passivate();
   getInnermostDelegate().close();
  } catch(SQLException e) {
   throw e;
  } catch(RuntimeException e) {
   throw e;
  } catch(Exception e) {
   throw (SQLException) new SQLException("Cannot close connection (return to pool failed)").initCause(e);
  }
 } else {
  // Abnormal close: underlying connection closed unexpectedly, so we
  // must destroy this proxy
  try {
   _pool.invalidateObject(this); // XXX should be guarded to happen at most once
  } catch(IllegalStateException e) {
   // pool is closed, so close the connection
   passivate();
   getInnermostDelegate().close();
  } catch (Exception ie) {
   // DO NOTHING, "Already closed" exception thrown below
  }
  throw new SQLException("Already closed.");
 }
}

一行_pool.returnObject(this)表明并非真的關閉了,而是返還給了連接池。

 到這里, PoolableConnectionFactory創(chuàng)建好了,它使用driverConnectionFactory來創(chuàng)建底層連接,通過makeObject來創(chuàng)建PoolableConnection,這個PoolableConnection通過與connectionPool關聯(lián)來達到改變close方法的作用,當PoolableConnectionFactory創(chuàng)建好的時候,它自己已經作為一個工廠類被設置到了connectionPool,后面connectionPool會使用這個工廠來生產PoolableConnection,而生成的所有的PoolableConnection都與connectionPool關聯(lián)起來了,可以從connectionPool取出,也可以還給connectionPool。接下來,讓我們來看一看到底怎么去初始化connectionPool。

 創(chuàng)建數(shù)據(jù)源并初始化連接池

createDataSourceInstance();
 
try {
 for (int i = 0 ; i < initialSize ; i++) {
  connectionPool.addObject();
 }
 } catch (Exception e) {
  throw new SQLNestedException("Error preloading the connection pool", e);
 }

我們先看 createDataSourceInstance();

protected void createDataSourceInstance() throws SQLException {
 PoolingDataSource pds = new PoolingDataSource(connectionPool);
 pds.setAccessToUnderlyingConnectionAllowed(isAccessToUnderlyingConnectionAllowed());
 pds.setLogWriter(logWriter);
 dataSource = pds;
}

其實就是創(chuàng)建一個PoolingDataSource,作為底層真正的數(shù)據(jù)源,這個PoolingDataSource比較簡單,這里不做詳細介紹

接下來是一個for循環(huán),通過調用connectionPool.addObject();來為連接池添加數(shù)據(jù)庫連接,下面是一張時序圖:

可以看出,在3.5中創(chuàng)建的PoolableConnectionFactory在這里起作用了,addObject依賴的正是makeObject,而makeObject在上面也介紹過了。

到此為止,數(shù)據(jù)源創(chuàng)建好了,連接池里也有了可以使用的連接,而且每個連接和連接池都做了關聯(lián),改變了close的行為。這個時候BasicDataSource正是可以工作了,調用getConnection的時候,實際是調用底層數(shù)據(jù)源的getConnection,而底層數(shù)據(jù)源其實就是從連接池中獲取的連接。

四.總結

 整個數(shù)據(jù)源最核心的其實就三個東西:

一個是連接池,在這里體現(xiàn)為common-pool中的GenericObjectPool,它負責緩存和管理連接,所有的配置策略都是由它管理。

第二個是連接,這里的連接就是PoolableConnection,當然它是對底層連接進行了封裝。

第三個則是連接池和連接的關系,在此表現(xiàn)為一對多的互相引用。對數(shù)據(jù)源的構建則是對連接池,連接以及連接池與連接的關系的構建,掌握了這些點,就基本能掌握數(shù)據(jù)源的構建。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI