锁定老帖子 主题:ibatis2.3源码之数据源&连接池浅析
精华帖 (0) :: 良好帖 (8) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2010-07-14
最后修改:2010-10-14
题记: 人越来越懒了,最近看代码的速度越来越慢了,加速。 数据源(datasource包&jdbc包)
在datasource包中ibatis提供三类对外数据源factory,分别为:SimpleDataSourceFactory、JndiDataSourceFactory、DbcpDataSourceFactory。
SimpleDataSourceFactory
SimpleDataSourceFactory对外提供简单数据源,接口定义如下:
public class SimpleDataSourceFactory implements DataSourceFactory { private DataSource dataSource; public void initialize(Map map) {// map是从sqlMapConfig.xml配置文件拆分出来的配置信息,用于初始化数据源。 dataSource = new SimpleDataSource(map); } } 工厂定义很简单,主要对外提供一个javax.sql.DataSource数据源。
对于数据源来说,配置信息涉及到: // Required Properties private static final String PROP_JDBC_DRIVER = "JDBC.Driver"; private static final String PROP_JDBC_URL = "JDBC.ConnectionURL"; private static final String PROP_JDBC_USERNAME = "JDBC.Username"; private static final String PROP_JDBC_PASSWORD = "JDBC.Password"; private static final String PROP_JDBC_DEFAULT_AUTOCOMMIT = "JDBC.DefaultAutoCommit"; // Optional Properties private static final String PROP_POOL_MAX_ACTIVE_CONN = "Pool.MaximumActiveConnections"; private static final String PROP_POOL_MAX_IDLE_CONN = "Pool.MaximumIdleConnections"; .... 典型的数据源配置如下: <transactionManager type="JDBC"> <dataSource type="DBCP"> <property name="JDBC.Driver" value="${driver}"/> <property name="JDBC.ConnectionURL" value="${url}"/> <property name="JDBC.Username" value="${username}"/> <property name="JDBC.Password" value="${password}"/> <property name="Pool.MaximumActiveConnections" value="8"/> <property name="Pool.MaximumIdleConnections" value="8"/> .... </dataSource> </transactionManager>
知道了需要配置的参数后,第一件是就是调用SimpleDataSource的initialize(Map props)做初始化工作,主要需要校验、设置默认值或赋值等。 大堆的初始化工作中就不说了,注意到initialize方法里有个driverProps变量,用于存储用户在配置中以Driver.开头的配置,如<property name="Driver.xxx" values="xxx" />,在建立连接池的时候,加载这些配置(DriverManager.getConnection(jdbcUrl, driverProps))。
JndiDataSourceFactory 同样ibatis也支持JNDI来初始化datasource,主要用于让服务器容器管理连接池。同样初始化工作、获取数据源实现如下: public void initialize(Map properties) { try { InitialContext initCtx = null; Hashtable context = getContextProperties(properties); if (context == null) { initCtx = new InitialContext(); } else { initCtx = new InitialContext(context); } if (properties.containsKey("DataSource")) { dataSource = (DataSource) initCtx.lookup((String) properties.get("DataSource")); } else if (properties.containsKey("DBJndiContext")) { // LEGACY --Backward compatibility dataSource = (DataSource) initCtx.lookup((String) properties.get("DBJndiContext")); } else if (properties.containsKey("DBFullJndiContext")) { // LEGACY --Backward compatibility dataSource = (DataSource) initCtx.lookup((String) properties.get("DBFullJndiContext")); } else if (properties.containsKey("DBInitialContext") && properties.containsKey("DBLookup")) { // LEGACY --Backward compatibility Context ctx = (Context) initCtx.lookup((String) properties.get("DBInitialContext")); dataSource = (DataSource) ctx.lookup((String) properties.get("DBLookup")); } } catch (NamingException e) { throw new SqlMapException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e); } }
DbcpDataSourceFactory 使用了第三方apache的dbcp来管理连接池。工厂接口很简单,如下: public class DbcpDataSourceFactory implements DataSourceFactory { private DataSource dataSource; public void initialize(Map map) { DbcpConfiguration dbcp = new DbcpConfiguration(map); dataSource = dbcp.getDataSource(); } }
加载dbcp的时候,有个判断配置Map是否含有“JDBC.Driver“属性,如下: BasicDataSource basicDataSource = null; if (map.containsKey("JDBC.Driver")) { basicDataSource = new BasicDataSource(); String driver = (String) map.get("JDBC.Driver"); String url = (String) map.get("JDBC.ConnectionURL"); String username = (String) map.get("JDBC.Username"); ... 如果有:则正常加载所有属性(源码省略),这里注意IBATIS只为DBCP加载一定量的配置,其他DBCP配置请以Driver.开头。 如果没有JDBC.Driver,则利用反射的知识进行赋值,在赋值的时候ibatis做了点类型转换的工作,因为源数据都是String字符类型,需要反射invoke到方法里,需要做类型变换,如下:
private BasicDataSource newDbcpConfiguration(Map map) { BasicDataSource basicDataSource = new BasicDataSource(); Iterator props = map.keySet().iterator(); while (props.hasNext()) { String propertyName = (String) props.next(); if (PROBE.hasWritableProperty(basicDataSource, propertyName))//判断basticDataSource对象有没有propertyName属性 { String value = (String) map.get(propertyName); Object convertedValue = convertValue(basicDataSource, propertyName, value);// 将value类型转换成basicDataSource对象propertyName变量的类型 PROBE.setObject(basicDataSource, propertyName, convertedValue); } } return basicDataSource; } private Object convertValue(Object object, String propertyName, String value) { Object convertedValue = value; Class targetType = PROBE.getPropertyTypeForSetter(object, propertyName);// 获取object对象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; }
以上关于ibatis三类数据源加载就完成了,对于加载数据源,看到ibatis基本没有什么限制,甚至可以不配置任何数据源信息,这为外部应用加载其他数据源提供了很大灵活性。
连接池(SimpleDataSource)
对于JNDIDataSource和DBCP都有自己的连接池管理,而SimpleDataSource由ibatis自己管理着连接,所有需要有自己的实现。在上面创建SimpleDataSource的时,ibatis并不马上建立自己的连接池的,而是在第一次使用Connection时触发连接池的创建。
看看 public Connection getConnection()方法: public Connection getConnection() throws SQLException { return popConnection(jdbcUsername, jdbcPassword).getProxyConnection(); } ibatis的连接池由2个数组分别存放空闲连接和非空闲连接: private final Object POOL_LOCK = new Object(); private List idleConnections = new ArrayList(); private List activeConnections = new ArrayList();
ibatis的连接池实现大致解读为:
SimplePooledConnection conn = null; while (conn == null) { synchronized (POOL_LOCK) { if (idleConnections.size() > 0) { // 有空闲连接,从池中取,这里因为用的ArrayList,效率有待提高,remove会触发数组的复制。 conn = (SimplePooledConnection) idleConnections.remove(0); } else { // 无空闲连接且活动连接小于最大活动数,则创建新的连接池 if (activeConnections.size() < poolMaximumActiveConnections) { // Can create new connection if (useDriverProps) { conn = new SimplePooledConnection(DriverManager.getConnection(jdbcUrl, driverProps), this); } else { conn = new SimplePooledConnection(DriverManager.getConnection(jdbcUrl, jdbcUsername, jdbcPassword), this); } } else { // Cannot create new connection,当前活动连接数大于最大值,不能创建新连接。 SimplePooledConnection oldestActiveConnection = (SimplePooledConnection) activeConnections.get(0); long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); // 尝试移除oldest活动连接,判断是否超时 if (longestCheckoutTime > poolMaximumCheckoutTime) { // Can claim overdue connection ... if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { oldestActiveConnection.getRealConnection().rollback();// 回滚 } conn = new SimplePooledConnection(oldestActiveConnection.getRealConnection(), this); oldestActiveConnection.invalidate();// 当前连接已经超时却为活动状态,判为无效。 } else { // Must wait 没有可用的连接池,最坏情况,会造成当先线程等待 try { if (!countedWait) { hadToWaitCount++; countedWait = true; } long wt = System.currentTimeMillis(); POOL_LOCK.wait(poolTimeToWait); accumulatedWaitTime += System.currentTimeMillis() - wt; } catch (InterruptedException e) { break; } } } } if (conn != null) { // 非新建立连接,从空闲队列中取的连接池,需要重置状态 if (conn.isValid()) { if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } conn.setConnectionTypeCode(assembleConnectionTypeCode(jdbcUrl, username, password)); conn.setCheckoutTimestamp(System.currentTimeMillis()); conn.setLastUsedTimestamp(System.currentTimeMillis()); activeConnections.add(conn); requestCount++; accumulatedRequestTime += System.currentTimeMillis() - t; } else { badConnectionCount++; localBadConnectionCount++; conn = null; if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {// 当前活动连接超时成为“坏”连接,抛出程序异常。 throw new SQLException("SimpleDataSource: Could not get a good connection to the database."); } } } } }
再看看pushConnection连接返回连接池操作,能看出ibatis池的一点异同。 private void pushConnection(SimplePooledConnection conn)
throws SQLException {
synchronized (POOL_LOCK) {
activeConnections.remove(conn);// 从活动队列移除conn
if (conn.isValid()) {
if (idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == getExpectedConnectionTypeCode()) {
accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
SimplePooledConnection newConn = new SimplePooledConnection(conn.getRealConnection(), this);// 精华,把移除的conn中的Connection重新赋值给新的SimplePooledConnection,而原来的SimplePooledConnection对象会销毁。
idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
conn.invalidate();
POOL_LOCK.notifyAll();
} else {
accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
conn.getRealConnection().close();
conn.invalidate();
}
} else {
badConnectionCount++;
}
}
}
以上的精华就在:SimplePooledConnection newConn = new SimplePooledConnection(conn.getRealConnection(), this);// 精华,把移除的conn中的Connection重新赋值给新的SimplePooledConnection,而原来的SimplePooledConnection对象会销毁。 对于对象池操作,如果要把老对象返回到池中,必定需要做清理工作,而ibatis的连接池在做返回池中并没有保留老对象,而是直接摒弃老对象,new一个新对象且载入老对象的Connection入到idleConnections队列中。
注意这里的remove()操作只是去除了引用,而非内存对象,GC暂时不会回收:) 推荐对于非容器管理连接池的话,用DBCP。
事务(transaction)
ibatis涉及到3类事务,分别为JDBC事务,JTA事务,可扩展事务。 说道事务一般会涉及到事务的接口、状态、配置、分布式事务等。ibatis提供的事务接口看上去很简单: public interface Transaction { public void commit() throws SQLException, TransactionException; public void rollback() throws SQLException, TransactionException; public void close() throws SQLException, TransactionException; public Connection getConnection() throws SQLException, TransactionException; }
事务状态有:STATE_STARTED、STATE_COMMITTED、STATE_ENDED、STATE_USER_PROVIDED 事务配置有: public interface TransactionConfig { public DataSource getDataSource(); public void setDataSource(DataSource ds); public void initialize(Properties props) throws SQLException, TransactionException; public Transaction newTransaction(int transactionIsolation) throws SQLException, TransactionException; public int getMaximumConcurrentTransactions(); public void setMaximumConcurrentTransactions(int maximumConcurrentTransactions); }
JDBC事务
public class JdbcTransaction implements Transaction { private static final Log connectionLog = LogFactory.getLog(Connection.class); private DataSource dataSource; private Connection connection; private IsolationLevel isolationLevel = new IsolationLevel(); public JdbcTransaction(DataSource ds, int isolationLevel) throws TransactionException { // Check Parameters dataSource = ds; if (dataSource == null) { throw new TransactionException("JdbcTransaction initialization failed. DataSource was null."); } this.isolationLevel.setIsolationLevel(isolationLevel); } private void init() throws SQLException, TransactionException { // Open JDBC Transaction connection = dataSource.getConnection(); if (connection == null) { throw new TransactionException("JdbcTransaction could not start transaction. Cause: The DataSource returned a null connection."); } // Isolation Level isolationLevel.applyIsolationLevel(connection); // AutoCommit if (connection.getAutoCommit()) { connection.setAutoCommit(false); } // Debug if (connectionLog.isDebugEnabled()) { connection = ConnectionLogProxy.newInstance(connection); } } public void commit() throws SQLException, TransactionException { if (connection != null) { connection.commit(); } } public void rollback() throws SQLException, TransactionException { if (connection != null) { connection.rollback(); } } public void close() throws SQLException, TransactionException { if (connection != null) { try { isolationLevel.restoreIsolationLevel(connection); } finally { connection.close(); connection = null; } } } public Connection getConnection() throws SQLException, TransactionException { if (connection == null) { init(); } return connection; } }
JTA事务(JTA事务一般由第三方实现,ibatis不关心实现)
public class JtaTransaction implements Transaction { private static final Log connectionLog = LogFactory.getLog(Connection.class); private UserTransaction userTransaction; private DataSource dataSource; private Connection connection; private IsolationLevel isolationLevel = new IsolationLevel(); private boolean commmitted = false; private boolean newTransaction = false; public JtaTransaction(UserTransaction utx, DataSource ds, int isolationLevel) throws TransactionException { // Check parameters userTransaction = utx; dataSource = ds; if (userTransaction == null) { throw new TransactionException("JtaTransaction initialization failed. UserTransaction was null."); } if (dataSource == null) { throw new TransactionException("JtaTransaction initialization failed. DataSource was null."); } this.isolationLevel.setIsolationLevel(isolationLevel); } private void init() throws TransactionException, SQLException { // Start JTA Transaction try { newTransaction = userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION; if (newTransaction) { userTransaction.begin(); } } catch (Exception e) { throw new TransactionException("JtaTransaction could not start transaction. Cause: ", e); } // Open JDBC Connection connection = dataSource.getConnection(); if (connection == null) { throw new TransactionException("JtaTransaction could not start transaction. Cause: The DataSource returned a null connection."); } // Isolation Level isolationLevel.applyIsolationLevel(connection); // AutoCommit if (connection.getAutoCommit()) { connection.setAutoCommit(false); } // Debug if (connectionLog.isDebugEnabled()) { connection = ConnectionLogProxy.newInstance(connection); } } public void commit() throws SQLException, TransactionException { if (connection != null) { if (commmitted) { throw new TransactionException("JtaTransaction could not commit because this transaction has already been committed."); } try { if (newTransaction) { userTransaction.commit(); } } catch (Exception e) { throw new TransactionException("JtaTransaction could not commit. Cause: ", e); } commmitted = true; } } public void rollback() throws SQLException, TransactionException { if (connection != null) { if (!commmitted) { try { if (userTransaction != null) { if (newTransaction) { userTransaction.rollback(); } else { userTransaction.setRollbackOnly(); } } } catch (Exception e) { throw new TransactionException("JtaTransaction could not rollback. Cause: ", e); } } } } public void close() throws SQLException, TransactionException { if (connection != null) { try { isolationLevel.restoreIsolationLevel(connection); } finally { connection.close(); connection = null; } } } public Connection getConnection() throws SQLException, TransactionException { if (connection == null) { init(); } return connection; } }
自定义事务
public class ExternalTransaction implements Transaction { private static final Log connectionLog = LogFactory.getLog(Connection.class); private DataSource dataSource; private boolean defaultAutoCommit; private boolean setAutoCommitAllowed; private Connection connection; private IsolationLevel isolationLevel = new IsolationLevel(); public ExternalTransaction(DataSource ds, boolean defaultAutoCommit, boolean setAutoCommitAllowed, int isolationLevel) throws TransactionException { // Check Parameters dataSource = ds; if (dataSource == null) { throw new TransactionException("ExternalTransaction initialization failed. DataSource was null."); } this.defaultAutoCommit = defaultAutoCommit; this.setAutoCommitAllowed = setAutoCommitAllowed; this.isolationLevel.setIsolationLevel(isolationLevel); } private void init() throws SQLException, TransactionException { // Open JDBC Transaction connection = dataSource.getConnection(); if (connection == null) { throw new TransactionException("ExternalTransaction could not start transaction. Cause: The DataSource returned a null connection."); } // Isolation Level isolationLevel.applyIsolationLevel(connection); // AutoCommit if (setAutoCommitAllowed) { if (connection.getAutoCommit() != defaultAutoCommit) { connection.setAutoCommit(defaultAutoCommit); } } // Debug if (connectionLog.isDebugEnabled()) { connection = ConnectionLogProxy.newInstance(connection); } } public void commit() throws SQLException, TransactionException { } public void rollback() throws SQLException, TransactionException { } public void close() throws SQLException, TransactionException { if (connection != null) { try { isolationLevel.restoreIsolationLevel(connection); } finally { connection.close(); connection = null; } } } public Connection getConnection() throws SQLException, TransactionException { if (connection == null) { init(); } return connection; } }
可以看出TransactionConfig封装了Connection的细节部分,每个TransactionConfig实例绑定着一个Connection,这个Connection是从ds中获取到的。
有了配置文件,事务状态,隔离级别,事务异常,就开始使用了,TrasactionManger就是来调度所有这些上下文Config。其中有begin,commit,end主要的三个方法,其中begin是新建一个Config并植入session对象且定义当前session的事务状态,commit则提交当前session的事务,end显然就结束当前session事务。如下:
public class TransactionManager { private TransactionConfig transactionConfig; private boolean forceCommit; private Throttle txThrottle; public TransactionManager(TransactionConfig transactionConfig) { this.transactionConfig = transactionConfig; this.txThrottle = new Throttle(transactionConfig.getMaximumConcurrentTransactions()); } public void begin(SessionScope session) throws SQLException, TransactionException { begin(session, IsolationLevel.UNSET_ISOLATION_LEVEL); } public void begin(SessionScope session, int transactionIsolation) throws SQLException, TransactionException { Transaction trans = session.getTransaction(); TransactionState state = session.getTransactionState(); if (state == TransactionState.STATE_STARTED) { throw new TransactionException("TransactionManager could not start a new transaction. " + "A transaction is already started."); } else if (state == TransactionState.STATE_USER_PROVIDED) { throw new TransactionException("TransactionManager could not start a new transaction. " + "A user provided connection is currently being used by this session. " + "The calling .setUserConnection (null) will clear the user provided transaction."); } txThrottle.increment(); try { trans = transactionConfig.newTransaction(transactionIsolation); session.setCommitRequired(false); } catch (SQLException e) { txThrottle.decrement(); throw e; } catch (TransactionException e) { txThrottle.decrement(); throw e; } session.setTransaction(trans); session.setTransactionState(TransactionState.STATE_STARTED); } public void commit(SessionScope session) throws SQLException, TransactionException { Transaction trans = session.getTransaction(); TransactionState state = session.getTransactionState(); if (state == TransactionState.STATE_USER_PROVIDED) { throw new TransactionException("TransactionManager could not commit. " + "A user provided connection is currently being used by this session. " + "You must call the commit() method of the Connection directly. " + "The calling .setUserConnection (null) will clear the user provided transaction."); } else if (state != TransactionState.STATE_STARTED && state != TransactionState.STATE_COMMITTED ) { throw new TransactionException("TransactionManager could not commit. No transaction is started."); } if (session.isCommitRequired() || forceCommit) { trans.commit(); session.setCommitRequired(false); } session.setTransactionState(TransactionState.STATE_COMMITTED); } public void end(SessionScope session) throws SQLException, TransactionException { Transaction trans = session.getTransaction(); TransactionState state = session.getTransactionState(); if (state == TransactionState.STATE_USER_PROVIDED) { throw new TransactionException("TransactionManager could not end this transaction. " + "A user provided connection is currently being used by this session. " + "You must call the rollback() method of the Connection directly. " + "The calling .setUserConnection (null) will clear the user provided transaction."); } try { if (trans != null) { try { if (state != TransactionState.STATE_COMMITTED) { if (session.isCommitRequired() || forceCommit) { trans.rollback(); session.setCommitRequired(false); } } } finally { session.closePreparedStatements(); trans.close(); } } } finally { if (state != TransactionState.STATE_ENDED) { txThrottle.decrement(); } session.setTransaction(null); session.setTransactionState(TransactionState.STATE_ENDED); } } public DataSource getDataSource() { return transactionConfig.getDataSource(); } public void setDataSource(DataSource ds) { transactionConfig.setDataSource(ds); } public boolean isForceCommit() { return forceCommit; } public void setForceCommit(boolean forceCommit) { this.forceCommit = forceCommit; } }
所以关于事务的上下文控制在ibatis的scope包里面了。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-07-15
最后修改:2010-07-15
C_J 写道
疑问:这句activeConnections.remove(conn);是否存在一定的风险?,ArrayList的remove操作为: elementData[--size] = null; // Let gc do its work 而实际上这个conn对象还需要被下面用到,如果GC恰好回收对象,是否会出现NULLPointerException异常呢?
不会引发NullPointerException ,因为GC回收的是数组持有的过期引用,而并非是回收conn对象。 这种方式在<Effective Java>第一版(第2章 第5条)中是被推荐的. |
|
返回顶楼 | |
发表时间:2010-07-15
我刚写了个小程序,一个数据库连接,顺序打开了N个statement,有查,有添加,有删。
有没有能写出监控驱动程序和数据库服务器交互的程序,这要知道驱动程序和服务器交互的协议,我想写个这个程序,看看到底事务怎么做,批量怎么做。手动提交,自动提交怎么做。可无从下手哟。 |
|
返回顶楼 | |
发表时间:2010-07-15
最后修改:2010-07-16
gogole_09 写道
C_J 写道
疑问:这句activeConnections.remove(conn);是否存在一定的风险?,ArrayList的remove操作为: elementData[--size] = null; // Let gc do its work 而实际上这个conn对象还需要被下面用到,如果GC恰好回收对象,是否会出现NULLPointerException异常呢?
不会引发NullPointerException ,因为GC回收的是数组持有的过期引用,而并非是回收conn对象。 这种方式在<Effective Java>第一版(第2章 第5条)中是被推荐的.
………………………………………………cutline
回楼上,你要实现JDBC接口吗?
|
|
返回顶楼 | |
发表时间:2010-07-16
您在《RE:ibatis2.3源码之数据源&连接池浅析(待续)》的回贴,内容为:
引用 我也想学习,晚上回去研究下你的成果! 被JavaEye用户投票评为差帖,积分-30分。 这有可能是因为你的回贴是灌水性回贴,JavaEye严禁灌水性回贴,您在发贴前请仔细阅读 JavaEye版规和提问的智慧,如有异议,可以在JavaEye站务讨论圈子申诉。 唉,早上做了十多分钟的测试,才能回复 我晚上真回去认真看了你的内容的, 上面的那个SimpleDataSourceFactory是属于简单工厂,还是抽象工厂?这种工厂,我看不出比直接new SimpldeDataSource好哟? |
|
返回顶楼 | |
发表时间:2010-07-16
2.3有一个问题,不知道楼主发现没有,调用 sybase的存储过程,如果返回多个结果集,那么在存储过程的2个select之间,不能出现set语句,如果出现set,后面的结果集就出不来了
|
|
返回顶楼 | |
发表时间:2010-07-16
回楼上:
简单工厂,虽然取名叫Factory,但我也感觉有点怪,这里的数据源为后面的事务模块服务,所以现在还未能完全解答你的疑问。 ……………………………………………………………… 嗯,ibatis2.3好像还是有很多BUG和有待优化的地方,像连接池的用的ArrayList,像delete from xxx where 1=1的问题等,但不知道3.0版本如何。 |
|
返回顶楼 | |
浏览 8057 次