Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Analysis of MyBatis Source Code

2025-03-29 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Database >

Share

Shulou(Shulou.com)06/01 Report--

What is MyBatis?

MyBatis is an excellent persistence layer framework that supports customized SQL, stored procedures, and advanced mapping. MyBatis avoids almost all JDBC code and manually setting parameters and extracting result sets. MyBatis uses simple XML or annotations to configure and map primitives, mapping interfaces and Java's POJOs (Plain Old Java Objects, ordinary Java objects) to records in the database.

Simple example of MyBatis

Although the XML file is generally used when using MyBatis, in order to analyze the simplicity of the program, the simple test program will not include the XML configuration. The test program includes an interface and a startup class:

Public interface UserMapper {@ Select ("SELECT * FROM user WHERE id = # {id}") User selectUser (int id);} public class Test2 {public static void main (String [] args) {SqlSessionFactory sqlSessionFactory = initSqlSessionFactory (); SqlSession session = sqlSessionFactory.openSession () Try {User user = (User) session.selectOne ("org.mybatis.example.UserMapper.selectUser", 1); System.out.println (user.getUserAddress ()); System.out.println (user.getUserName ()) } finally {session.close ();}} private static SqlSessionFactory initSqlSessionFactory () {DataSource dataSource = new PooledDataSource ("com.mysql.jdbc.Driver", "jdbc:mysql://127.0.0.1:3306/jdbc", "root", "") TransactionFactory transactionFactory = new JdbcTransactionFactory (); Environment environment = new Environment ("development", transactionFactory, dataSource); Configuration configuration = new Configuration (environment); configuration.addMapper (UserMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder () .build (configuration) Return sqlSessionFactory;}}

UserMapper is an interface that we registered with sqlSessionFactory through configuration.addMapper (UserMapper.class) when we built the sqlSessionFactory. From the above code, we can see that to use MyBatis, we should go through the following steps: 1, create sqlSessionFactory (one-time operation); 2, use sqlSessionFactory object to construct sqlSession object; 3, call the corresponding method of sqlSession; 4, close sqlSession object.

In the main method, we do not configure sql, nor do we assemble objects according to the query results, we just need to pass in a namespace and method parameter parameters when calling the sqlSession method, and all operations are object-oriented. In the UserMapper interface, we customize our own sql,MyBatis and give us the right to write sql, which is convenient for us to optimize sql and debug sql.

Review of JDBC Foundation

It is very painful to use JDBC directly. Connecting to the database with JDBC includes the following basic steps: 1. Register driver; 2. Establish connection (Connection); 3. Create SQL statement (Statement); 4. Execute statement; 5. Process execution result (ResultSet); 6. Release resources. Sample code is as follows:

Test () SQLException {.forName (); Connection conn = DriverManager.getConnection (,); Statement st = conn.createStatement (); ResultSet rs = st.executeQuery (); (rs. ()) {User user = User (rs.getObject (), rs.getObject ());} rs.close () St.close (); conn.close ();}

You can see that MyBatis simplifies a lot of work for us compared to using JDBC directly:

1. Abstract the work related to creating a connection into a sqlSessionFactory object, which can be used many times at a time.

2. Split the sql statement from the business layer to make the code logic clearer and increase maintainability.

3, automatically complete the result set processing, there is no need for us to write repetitive code.

However, we should know that although the framework can help us simplify our work, the underlying code of the framework is definitely the most basic JDBC code, because this is a common way for the Java platform to connect to the database. Today I will analyze the MyBatis source code to see how MyBatis encapsulates this basic code into a framework.

MyBatis call process

What we end up calling is the method on the sqlSession object, so we first trace the creation method of sqlSession: sqlSessionFactory.openSession (), which eventually calls the following method of DefaultSqlSessionFactory:

Private SqlSession openSessionFromDataSource (ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null; try {final Environment environment = configuration.getEnvironment (); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment (environment); tx = transactionFactory.newTransaction (environment.getDataSource (), level, autoCommit); final Executor executor = configuration.newExecutor (tx, execType); return new DefaultSqlSession (configuration, executor, autoCommit);} catch (Exception e) {closeTransaction (tx) / / may have fetched a connection so lets call close () throw ExceptionFactory.wrapException ("Error opening session. Cause: "+ e, e);} finally {ErrorContext.instance () .reset ();}}

The final returned object is a DefaultSqlSession object. In debug mode, we see that autoCommit is false,executor and CachingExecutor is CachingExecutor, and there is an attribute delegate in CachingExecutor whose type is simpleExecutor:

Now, let's follow up the selectOne () method of DefaultSqlSession to see the call flow of this method, and the selectOne () method will call the selectList () method:

Public List selectList (String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms = configuration.getMappedStatement (statement); List result = executor.query (ms, wrapCollection (parameter), rowBounds, Executor.NO_RESULT_HANDLER); return result;} catch (Exception e) {throw ExceptionFactory.wrapException ("Error querying database. Cause:" + e, e);} finally {ErrorContext.instance (). Reset ();}}

You can see that in order to get the query result, you finally have to call the query method on executor, where executor is the CachingExecutor instance, and the follower gets the following code:

Public List query (MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql (parameterObject); CacheKey key = createCacheKey (ms, parameterObject, rowBounds, boundSql); return query (ms, parameterObject, rowBounds, resultHandler, key, boundSql);} public List query (MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {Cache cache = ms.getCache (); if (cache! = null) {flushCacheIfRequired (flushCacheIfRequired) If (ms.isUseCache () & & resultHandler = = null) {ensureNoOutParams (ms, parameterObject, boundSql); @ SuppressWarnings ("unchecked") List list = (List) tcm.getObject (cache, key); if (list = = null) {list = delegate. Query (ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject (cache, key, list); / / issue # 578. Query must be not synchronized to prevent deadlocks} return list;}} return delegate. Query (ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

The MyBatis framework first generates a boundSql and CacheKey with the sql statement we passed in in the boundSql:

After generating boundSql and CacheKey, an overloaded function is called. In the overloaded function, we will check whether there is a cache. This cache is the secondary cache of MyBatis. If we have not configured it, call the last sentence delegate directly. Query (ms, parameterObject, rowBounds, resultHandler, key, boundSql). As mentioned earlier, this delagate is actually simpleExecutor. Go in and check it out:

Public List 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."); if (queryStack = = 0 & & ms.isFlushCacheRequired ()) {clearLocalCache ();} List list; try {queryStack++; list = resultHandler = = null? (List) 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) {for (DeferredLoad deferredLoad: deferredLoads) {deferredLoad.load ();} deferredLoads.clear () / / issue # 601 if (configuration.getLocalCacheScope () = = LocalCacheScope.STATEMENT) {clearLocalCache (); / / issue # 482}} return list;}

The key code is the following three lines:

List = resultHandler = = null? List) localCache.getObject (key): null; if (list! = null) {handleLocallyCachedOutputParameters (ms, key, parameter, boundSql);} else {list = queryFromDatabase (ms, parameter, rowBounds, resultHandler, key, boundSql);}

First, try to get List from localCache according to key, where localCache is the first-level cache of MyBatis. If not, call queryFromDatabase () to query from the database:

Private List queryFromDatabase (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List 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;}

The key code is to call the doQuery () code, and the doQuery () method of SimpleExecutor is as follows:

Public List doQuery (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null; try {Configuration configuration = ms.getConfiguration (); StatementHandler handler = configuration.newStatementHandler (wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement (handler, ms.getStatementLog ()); return handler.query (stmt, resultHandler);} finally {closeStatement (stmt);}}

The prepareStatement method is called, which is as follows:

Private Statement prepareStatement (StatementHandler handler, Log statementLog) throws SQLException {Statement stmt; Connection connection = getConnection (statementLog); stmt = handler.prepare (connection); handler.parameterize (stmt); return stmt;}

Finally, we see the familiar code, first Connection, then Statement from Connection, and in debug mode we see that our sql statement has been set to stmt:

Now that you have the Statement object and the sql is set up, all you need is execution and object mapping, continue to follow the code, and we'll trace it to org.apache.ibatis.executor.statement.

The executor method of the PreparedStatementHandler class:

Public List query (Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement; ps.execute (); return resultSetHandler. HandleResultSets (ps);}

Here, the ps.execute () method is called to execute sql, followed by the call to resultSetHandler. The handleResultSets (ps) method obviously encapsulates the result set, so I'm not going to follow up.

Database connection Pool for MyBatis

The above section describes the overall process of MyBatis execution, and this section intends to discuss a specific topic: database connection pooling for MyBatis.

We know that creating a Connection every time we connect to a database consumes performance, so when we write JDBC code, we usually use database connection pooling. The used Connection is not directly closed, but put into the database connection pool to facilitate next reuse. Open source database connection pooling includes DBCP, C3P0, etc. MyBatis also implements its own database connection pool. In this section I will explore the database connection pool source code implemented by MyBatis.

Following the getConnection () method in the previous section, we will eventually enter the getConnection () method of JdbcTransaction, the getConnection () method will call the openConnection () method, and openConnection () will call the getConnection () method of dataSource:

Public Connection getConnection () throws SQLException {if (connection = = null) {openConnection ();} return connection;} protected void openConnection () throws SQLException {if (log.isDebugEnabled ()) {log.debug ("Opening JDBC Connection");} connection = dataSource.getConnection (); if (level! = null) {connection.setTransactionIsolation (level.getLevel ()) } setDesiredAutoCommit (autoCommmit);}

The dataSource here is of type PooledDataSource. Follow up and check the source code as follows:

Public Connection getConnection () throws SQLException {return popConnection (dataSource.getUsername (), dataSource.getPassword ()). GetProxyConnection ();} private PooledConnection popConnection (String username, String password) throws SQLException {/ / Don't analyze}

You can see that the object we return here is no longer a native Connection object, but a dynamic proxy object, which is a property of PooledConnection. All operations on Connection objects will be intercepted by PooledConnection. We can see the definition of PooledConnection as follows:

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 checkoutTimest private long createdTimest private long lastUsedTimest private int connectionTypeCode; private boolean valid; 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) } public void invalidate () {valid = false;} public boolean isValid () {return valid & & realConnection! = null & & dataSource.pingConnection (this);} public Connection getRealConnection () {return realConnection;} public Connection getProxyConnection () {return proxyConnection } public int getRealHashCode () {if (realConnection = = null) {return 0;} else {return realConnection.hashCode ();}} public int getConnectionTypeCode () {return connectionTypeCode;} public void setConnectionTypeCode (int connectionTypeCode) {this.connectionTypeCode = connectionTypeCode } public long getCreatedTimestamp () {return createdTimest} public void setCreatedTimestamp (long createdTimestamp) {this.createdTimestamp = createdTimest} public long getLastUsedTimestamp () {return lastUsedTimest} public void setLastUsedTimestamp (long lastUsedTimestamp) {this.lastUsedTimestamp = lastUsedTimestamp } public long getTimeElapsedSinceLastUse () {return System.currentTimeMillis ()-lastUsedTimest} public long getAge () {return System.currentTimeMillis ()-createdTimest} public long getCheckoutTimestamp () {return checkoutTimest} public void setCheckoutTimestamp (long timestamp) {this.checkoutTimestamp = timestamp } public long getCheckoutTime () {return System.currentTimeMillis ()-checkoutTimest} public int hashCode () {return hashCode } public boolean equals (Object obj) {if (obj instanceof PooledConnection) {return realConnection.hashCode () = = ((PooledConnection) obj) .realConnection.hashCode ();} else if (obj instanceof Connection) {return hashCode = = obj.hashCode () } else {return false;}} public Object invoke (Object proxy, Method method, Object [] args) throws Throwable {String methodName = method.getName () If (CLOSE.hashCode () = = methodName.hashCode () & & CLOSE.equals (methodName)) {dataSource.pushConnection (this); return null } else {try {if (! Object.class.equals (method.getDeclaringClass () {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. ");}

You can see that this class exposes many interfaces to detect Connection status, such as whether the connection is valid, the most recent use of the connection when the connection was created, and so on:

This class implements the InvocationHandler interface, and the main method is as follows:

Public Object invoke (Object proxy, Method method, Object [] args) throws Throwable {String methodName = method.getName (); if (CLOSE.hashCode () = = methodName.hashCode () & & CLOSE.equals (methodName)) {dataSource.pushConnection (this); return null;} else {try {if (! Object.class.equals (method.getDeclaringClass () {checkConnection ();} return method.invoke (realConnection, args) } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable (t);}

As you can see, PooledConnection intercepts the close method, and when the client calls the close () method, the program does not close the Connection, but calls the dataSource.pushConnection (this) method, which is implemented as follows:

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++; } } } 可以看到,首先会把Connection从活跃列表中删除,然后检测空闲列表的长度有没有达到最大长度(默认为5),若没有达到,把Connection放入空闲链表,否则关闭连接。这里的state是一个PoolState对象,该对象定义如下: public class PoolState { protected PooledDataSource dataSource; protected final List idleConnections = new ArrayList(); protected final List activeConnections = new ArrayList(); 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; public PoolState(PooledDataSource dataSource) { this.dataSource = dataSource; } public synchronized long getRequestCount() { return requestCount; } public synchronized long getAverageRequestTime() { return requestCount == 0 ? 0 : accumulatedRequestTime / requestCount; } public synchronized long getAverageWaitTime() { return hadToWaitCount == 0 ? 0 : accumulatedWaitTime / hadToWaitCount; } public synchronized long getHadToWaitCount() { return hadToWaitCount; } public synchronized long getBadConnectionCount() { return badConnectionCount; } public synchronized long getClaimedOverdueConnectionCount() { return claimedOverdueConnectionCount; } public synchronized long getAverageOverdueCheckoutTime() { return claimedOverdueConnectionCount == 0 ? 0 : accumulatedCheckoutTimeOfOverdueConnections / claimedOverdueConnectionCount; } public synchronized long getAverageCheckoutTime() { return requestCount == 0 ? 0 : accumulatedCheckoutTime / requestCount; } public synchronized int getIdleConnectionCount() { return idleConnections.size(); } public synchronized int getActiveConnectionCount() { return activeConnections.size(); }} 可以看到最终我们的Connection对象是放在ArrayList中的,该类还提供一些接口返回连接池基本信息。 好了,现在我们可以回去看看PooledDataSource的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); @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++; 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;}

You can see that access to connection one is divided into the following situations: 1, if there is a free Connection, then directly use the free Connection, otherwise 2, if the active Connection does not reach the upper limit of the active Connection, then create a new Connection and return, otherwise 3, if the active Connection reaches the upper limit and the detected Connection is detected for too long, then set the Connection to invalidate and create a new Connection, otherwise 4posit 4, wait for idle Connection.

At this point, we have sorted out the database connection pool code of MyBatis, and there are two key points: 1, the Connection checked out is not a native Connection, but a proxy object; 2, the container for storing Connection is that ArrayList,Connection is checked out in accordance with the first-in-first-out principle.

This blog is very good, you can mark: × × / technology

Transactions of MyBatis

First, review the transaction knowledge of JDBC.

JDBC can operate the setAutoCommit () method of Connection, give it the false parameter, prompt the database to start the transaction, and after issuing a series of SQL commands, call the commit () method of Connection to prompt the database to confirm (Commit) operation. If an intermediate error occurs, rollback () is called to prompt the database to undo (ROLLBACK) all execution. At the same time, if you only want to recall a SQL execution point, you can set a save point (SAVEPOINT). A sample transaction flow is as follows:

Connection conn =...; Savepoint point = null;try {conn.setAutoCommit (false); Statement stmt = conn.createStatement (); stmt.executeUpdate ("INSERT INTO..."); Point = conn.setSavepoint (); stmt.executeUpdate ("INSERT INTO...");... Conn.commit ();} catch (SQLException e) {e.printStackTrace (); if (conn! = null) {try {if (point = = null) {conn.rollback ();} else {conn.rollback (point) Conn.releaseSavepoint (point);}} catch (SQLException ex) {ex.printStackTrace ();} finally {... If (conn! = null) {try {conn.setAutoCommit (true); conn.close ();} catch (SQLException ex) {ex.printStackTrace ();}

As written in the MyBatis call process section, in debug mode, we see that autoCommit is false, so each sqlSession is actually a transaction, which is why commit must be called every time you delete, change, and check.

Source code: × × / technology

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Database

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report