日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

statement执行insert into语句_【图文并茂】源码解析MyBatis ShardingJdbc SQL语句执行流程详解...

發布時間:2025/3/15 数据库 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 statement执行insert into语句_【图文并茂】源码解析MyBatis ShardingJdbc SQL语句执行流程详解... 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

源碼分析Mybatis系列目錄:
1、源碼分析Mybatis MapperProxy初始化【圖文并茂】
2、源碼分析Mybatis MappedStatement的創建流程
3、【圖文并茂】Mybatis執行SQL的4大基礎組件詳解

如果圖不清晰的話,可以查看CSDN博客鏈接:https://blog.csdn.net/prestigeding/article/details/90647674

本文將詳細介紹Mybatis SQL語句執行的全流程,本文與上篇具有一定的關聯性,建議先閱讀該系列中的前面3篇文章,重點掌握Mybatis Mapper類的初始化過程,因為在Mybatis中,Mapper是執行SQL語句的入口,類似下面這段代碼:

1@Service
2public?UserService?implements?IUserService?{
3?????@Autowired
4????private?UserMapper?userMapper;
5????public?User?findUser(Integer?id)?{
6????????return?userMapper.find(id);
7????}
8}

開始進入本文的主題,以源碼為手段,分析Mybatis執行SQL語句的流行,并且使用了數據庫分庫分表中間件sharding-jdbc,其版本為sharding-jdbc1.4.1。

為了方便大家對本文的源碼分析,先給出Mybatis層面核心類的方法調用序列圖。

SQL執行序列圖

源碼解析SQL執行流程

接下來從從源碼的角度對其進行剖析。

溫馨提示:在本文的末尾,還會給出一張詳細的Mybatis Shardingjdbc語句執行流程圖。(請勿錯過哦)。

2.1 MapperProxy#invoker

1public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Throwable?{
2????if?(Object.class.equals(method.getDeclaringClass()))?{
3??????try?{
4????????return?method.invoke(this,?args);
5??????}?catch?(Throwable?t)?{
6????????throw?ExceptionUtil.unwrapThrowable(t);
7??????}
8????}
9????final?MapperMethod?mapperMethod?=?cachedMapperMethod(method);???//?@1
10????return?mapperMethod.execute(sqlSession,?args);?????????????????????????????????????//?@2
11??}

代碼@1:創建并緩存MapperMethod對象。

代碼@2:調用MapperMethod對象的execute方法,即mapperInterface中定義的每一個方法最終會對應一個MapperMethod。

2.2 MapperMethod#execute

1public?Object?execute(SqlSession?sqlSession,?Object[]?args)?{
2????Object?result;
3????if?(SqlCommandType.INSERT?==?command.getType())?{?
4??????Object?param?=?method.convertArgsToSqlCommandParam(args);
5??????result?=?rowCountResult(sqlSession.insert(command.getName(),?param));
6????}?else?if?(SqlCommandType.UPDATE?==?command.getType())?{
7??????Object?param?=?method.convertArgsToSqlCommandParam(args);
8??????result?=?rowCountResult(sqlSession.update(command.getName(),?param));
9????}?else?if?(SqlCommandType.DELETE?==?command.getType())?{
10??????Object?param?=?method.convertArgsToSqlCommandParam(args);
11??????result?=?rowCountResult(sqlSession.delete(command.getName(),?param));
12????}?else?if?(SqlCommandType.SELECT?==?command.getType())?{
13??????if?(method.returnsVoid()?&&?method.hasResultHandler())?{
14????????executeWithResultHandler(sqlSession,?args);
15????????result?=?null;
16??????}?else?if?(method.returnsMany())?{
17????????result?=?executeForMany(sqlSession,?args);
18??????}?else?if?(method.returnsMap())?{
19????????result?=?executeForMap(sqlSession,?args);
20??????}?else?{
21????????Object?param?=?method.convertArgsToSqlCommandParam(args);
22????????result?=?sqlSession.selectOne(command.getName(),?param);
23??????}
24????}?else?{
25??????throw?new?BindingException("Unknown?execution?method?for:?"?+?command.getName());
26????}
27????if?(result?==?null?&&?method.getReturnType().isPrimitive()?&&?!method.returnsVoid())?{
28??????throw?new?BindingException("Mapper?method?'"?+?command.getName()?
29??????????+?"?attempted?to?return?null?from?a?method?with?a?primitive?return?type?("?+?method.getReturnType()?+?").");
30????}
31????return?result;
32??}

該方法主要是根據SQL類型,insert、update、select等操作,執行對應的邏輯,本文我們以查詢語句,進行跟蹤,進入executeForMany(sqlSession, args)方法。

2.3 MapperMethod#executeForMany

1private??Object?executeForMany(SqlSession?sqlSession,?Object[]?args)?{ 2????List?result; 3????Object?param?=?method.convertArgsToSqlCommandParam(args); 4????if?(method.hasRowBounds())?{ 5??????RowBounds?rowBounds?=?method.extractRowBounds(args); 6??????result?=?sqlSession.selectList(command.getName(),?param,?rowBounds); 7????}?else?{ 8??????result?=?sqlSession.selectList(command.getName(),?param); 9????}10????//?issue?#510?Collections?&?arrays?support11????if?(!method.getReturnType().isAssignableFrom(result.getClass()))?{12??????if?(method.getReturnType().isArray())?{13????????return?convertToArray(result);14??????}?else?{15????????return?convertToDeclaredCollection(sqlSession.getConfiguration(),?result);16??????}17????}18????return?result;19??}

該方法也比較簡單,最終通過SqlSession調用selectList方法。

2.4 DefaultSqlSession#selectList

1public??List?selectList(String?statement,?Object?parameter,?RowBounds?rowBounds)?{ 2????try?{ 3??????MappedStatement?ms?=?configuration.getMappedStatement(statement);???//?@1 4??????List?result?=?executor.query(ms,?wrapCollection(parameter),?rowBounds,?Executor.NO_RESULT_HANDLER);???//?@2 5??????return?result; 6????}?catch?(Exception?e)?{ 7??????throw?ExceptionFactory.wrapException("Error?querying?database.??Cause:?"?+?e,?e); 8????}?finally?{ 9??????ErrorContext.instance().reset();10????}11??}

代碼@1:根據資源名稱獲取對應的MappedStatement對象,此時的statement為資源名稱,例如com.demo.UserMapper.findUser。至于MappedStatement對象的生成在上一節初始化時已詳細介紹過,此處不再重復介紹。

代碼@2:調用Executor的query方法。這里說明一下,其實一開始會進入到CachingExecutor#query方法,由于CachingExecutor的Executor delegate屬性默認是SimpleExecutor,故最終還是會進入到SimpleExecutor#query中。

接下來我們進入到SimpleExecutor的父類BaseExecutor的query方法中。

2.5 BaseExecutor#query

1public??List?query(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{???//?@1 2????ErrorContext.instance().resource(ms.getResource()).activity("executing?a?query").object(ms.getId()); 3????if?(closed)?throw?new?ExecutorException("Executor?was?closed."); 4????if?(queryStack?==?0?&&?ms.isFlushCacheRequired())?{ 5??????clearLocalCache(); 6????} 7????List?list; 8????try?{ 9??????queryStack++;10??????list?=?resultHandler?==?null???(List)?localCache.getObject(key)?:?null;????????????????????????????????????????????//?@211??????if?(list?!=?null)?{12????????handleLocallyCachedOutputParameters(ms,?key,?parameter,?boundSql);13??????}?else?{14????????list?=?queryFromDatabase(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);???????????????????//?@315??????}16????}?finally?{17??????queryStack--;18????}19????if?(queryStack?==?0)?{20??????for?(DeferredLoad?deferredLoad?:?deferredLoads)?{21????????deferredLoad.load();22??????}23??????deferredLoads.clear();?//?issue?#60124??????if?(configuration.getLocalCacheScope()?==?LocalCacheScope.STATEMENT)?{?????????????????????????//?@425????????clearLocalCache();?//?issue?#48226??????}27????}28????return?list;29??}

代碼@1:首先介紹一下該方法的入參,這些類都是Mybatis的重要類:

  • MappedStatement ms
    映射語句,一個MappedStatemnet對象代表一個Mapper中的一個方法,是映射的最基本對象。

  • Object parameter
    SQL語句的參數列表。

  • RowBounds rowBounds
    行邊界對象,其實就是分頁參數limit與size。

  • ResultHandler resultHandler
    結果處理Handler。

  • CacheKey key
    Mybatis緩存Key

  • BoundSql boundSql
    SQL與參數綁定信息,從該對象可以獲取在映射文件中的SQL語句。

代碼@2:首先從緩存中獲取,Mybatis支持一級緩存(SqlSession)與二級緩存(多個SqlSession共享)。

代碼@3:從數據庫查詢結果,然后進入到doQuery方法,執行真正的查詢動作。

代碼@4:如果一級緩存是語句級別的,則語句執行完畢后,刪除緩存。

2.6 SimpleExecutor#doQuery

1public??List?doQuery(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?BoundSql?boundSql)?throws?SQLException?{ 2????Statement?stmt?=?null; 3????try?{ 4??????Configuration?configuration?=?ms.getConfiguration(); 5??????StatementHandler?handler?=?configuration.newStatementHandler(wrapper,?ms,?parameter,?rowBounds,?resultHandler,?boundSql);???//?@1 6??????stmt?=?prepareStatement(handler,?ms.getStatementLog());???????????????????????????????????????????????????????????????????????????????????????????????????????????????????//?@2 7??????return?handler.query(stmt,?resultHandler);????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????//?@3 8????}?finally?{ 9??????closeStatement(stmt);10????}11??}

代碼@1:創建StatementHandler,這里會加入Mybatis的插件擴展機制(將在下篇詳細介紹),如圖所示:

在這里插入圖片描述
代碼@2:創建Statement對象,注意,這里就是JDBC協議的java.sql.Statement對象了。

代碼@3:使用Statment對象執行SQL語句。

接下來詳細介紹Statement對象的創建過程與執行過程,即分布詳細跟蹤代碼@2與代碼@3。

Statement對象創建流程

3.1 java.sql.Connection對象創建

SimpleExecutor#prepareStatement

1private?Statement?prepareStatement(StatementHandler?handler,?Log?statementLog)?throws?SQLException?{
2????Statement?stmt;
3????Connection?connection?=?getConnection(statementLog);??//?@1
4????stmt?=?handler.prepare(connection);??????????????????????????????????//?@2
5????handler.parameterize(stmt);???????????????????????????????????????????????//?@3
6????return?stmt;
7}

創建Statement對象,分成三步:
代碼@1:創建java.sql.Connection對象。

代碼@2:使用Connection對象創建Statment對象。

代碼@3:對Statement進行額外處理,特別是PrepareStatement的參數設置(ParameterHandler)。

SimpleExecutor#getConnection

getConnection方法,根據上面流程圖所示,先是進入到SpringManagedTransaction,再通過spring-jdbc框架,利用DataSourceUtils獲取連接,其代碼如下:

1org.mybatis.spring.transaction.SpringManagedTransaction#doGetConnection 2public?static?Connection?doGetConnection(DataSource?dataSource)?throws?SQLException?{??
3????????Assert.notNull(dataSource,?"No?DataSource?specified");
4????????ConnectionHolder?conHolder?=?(ConnectionHolder)?TransactionSynchronizationManager.getResource(dataSource);?
5????????if?(conHolder?!=?null?&&?(conHolder.hasConnection()?||?conHolder.isSynchronizedWithTransaction()))?{
6????????????conHolder.requested();
7????????????if?(!conHolder.hasConnection())?{
8????????????????conHolder.setConnection(dataSource.getConnection());
9????????????}
10????????????return?conHolder.getConnection();
11????????}
12????????//?Else?we?either?got?no?holder?or?an?empty?thread-bound?holder?here.
13
14????????logger.debug("Fetching?JDBC?Connection?from?DataSource");
15????????Connection?con?=?dataSource.getConnection();??????//?@1
16
17????????//?這里省略與事務處理相關的代碼
18????????return?con;
19????}

代碼@1:通過DataSource獲取connection,那此處的DataSource是“誰”呢?看一下我們工程的配置:

故最終dataSouce.getConnection獲取的連接,是從SpringShardingDataSource中獲取連接。

1com.dangdang.ddframe.rdb.sharding.jdbc.ShardingDataSource#getConnection2public?ShardingConnection?getConnection()?throws?SQLException?{
3????????MetricsContext.init(shardingProperties);
4????????return?new?ShardingConnection(shardingContext);
5}

返回的結果如下:


備注:這里只是返回了一個ShardingConnection對象,該對象包含了分庫分表上下文,但此時并沒有執行具體的分庫操作(切換數據源)。

Connection的獲取流程清楚后,我們繼續來看一下Statemnet對象的創建。

3.2 java.sql.Statement對象創建

1stmt?=?prepareStatement(handler,?ms.getStatementLog());????????????

上面語句的調用鏈:RoutingStatementHandler -》BaseStatementHand

BaseStatementHandler#prepare

3public?Statement?prepare(Connection?connection)?throws?SQLException?{
4????ErrorContext.instance().sql(boundSql.getSql());
5????Statement?statement?=?null;
6????try?{
7??????statement?=?instantiateStatement(connection);????//?@1
8??????setStatementTimeout(statement);?????????????????????????//?@2
9??????setFetchSize(statement);??????????????????????????????????????//?@3
10??????return?statement;
11????}?catch?(SQLException?e)?{
12??????closeStatement(statement);
13??????throw?e;
14????}?catch?(Exception?e)?{
15??????closeStatement(statement);
16??????throw?new?ExecutorException("Error?preparing?statement.??Cause:?"?+?e,?e);
17????}
18??}

代碼@1:根據Connection對象(本文中是ShardingConnection)來創建Statement對象,其默認實現類:PreparedStatementHandler#instantiateStatement方法。

代碼@2:為Statement設置超時時間。

代碼@3:設置fetchSize。

1PreparedStatementHandler#instantiateStatement 2protected?Statement?instantiateStatement(Connection?connection)?throws?SQLException?{
3????String?sql?=?boundSql.getSql();
4????if?(mappedStatement.getKeyGenerator()?instanceof?Jdbc3KeyGenerator)?{
5??????String[]?keyColumnNames?=?mappedStatement.getKeyColumns();
6??????if?(keyColumnNames?==?null)?{
7????????return?connection.prepareStatement(sql,?PreparedStatement.RETURN_GENERATED_KEYS);
8??????}?else?{
9????????return?connection.prepareStatement(sql,?keyColumnNames);
10??????}
11????}?else?if?(mappedStatement.getResultSetType()?!=?null)?{
12??????return?connection.prepareStatement(sql,?mappedStatement.getResultSetType().getValue(),?ResultSet.CONCUR_READ_ONLY);
13????}?else?{
14??????return?connection.prepareStatement(sql);
15????}
16??}

其實Statement對象的創建,就比較簡單了,既然Connection是ShardingConnection,那就看一下其對應的prepareStatement方法即可。

ShardingConnection#prepareStatement

1 3public?PreparedStatement?prepareStatement(final?String?sql)?throws?SQLException?{???//?sql,為配置在mybatis?xml文件中的sql語句
4????????return?new?ShardingPreparedStatement(this,?sql);
5}
6ShardingPreparedStatement(final?ShardingConnection?shardingConnection,?
7????????????final?String?sql,?final?int?resultSetType,?final?int?resultSetConcurrency,?final?int?resultSetHoldability)?{
8????????super(shardingConnection,?resultSetType,?resultSetConcurrency,?resultSetHoldability);
9????????preparedSQLRouter?=?shardingConnection.getShardingContext().getSqlRouteEngine().prepareSQL(sql);
10}

在構建ShardingPreparedStatement對象的時候,會根據SQL語句創建解析SQL路由的解析器對象,但此時并不會執行相關的路由計算,PreparedStatement對象創建完成后,就開始進入SQL執行流程中。

SQL執行流程

接下來我們繼續看SimpleExecutor#doQuery方法的第3步,執行SQL語句:

1handler.query(stmt,?resultHandler)。

首先會進入RoutingStatementHandler這個類中,進行Mybatis層面的路由(主要是根據Statement類型)


然后進入到PreparedStatementHandler#query中。

PreparedStatementHandler#query


3public??List?query(Statement?statement,?ResultHandler?resultHandler)?throws?SQLException?{4????PreparedStatement?ps?=?(PreparedStatement)?statement;5????ps.execute();??//?@16????return?resultSetHandler.?handleResultSets(ps);??//?@27}

代碼@1:調用PreparedStatement的execute方法,由于本例是使用了Sharding-jdbc分庫分表,此時調用的具體實現為:ShardingPreparedStatement。

代碼@2:處理結果。

我們接下來分別來跟進execute與結果處理方法。

ShardingPreparedStatement#execute

2public?boolean?execute()?throws?SQLException?{
3????try?{
4????????return?new?PreparedStatementExecutor(getShardingConnection().getShardingContext().getExecutorEngine(),?routeSQL()).execute();?//?@1
5????}?finally?{
6????????clearRouteContext();
7????}
8}

這里奧妙無窮,其關鍵點如下:
1)創造PreparedStatementExecutor對象,其兩個核心參數:

  • ExecutorEngine executorEngine:shardingjdbc執行引擎。

  • Collection< PreparedStatementExecutorWrapper> preparedStatemenWrappers
    一個集合,每一個集合是PreparedStatement的包裝類,這個集合如何而來?

2)preparedStatemenWrappers是通過routeSQL方法產生的。

3)最終調用PreparedStatementExecutor方法的execute來執行。

接下來分別看一下routeSQL與execute方法。

ShardingPreparedStatement#routeSQL

3private?List?routeSQL()?throws?SQLException?{
4????????List?result?=?new?ArrayList<>(); 5????????SQLRouteResult?sqlRouteResult?=?preparedSQLRouter.route(getParameters());???//?@1 6????????MergeContext?mergeContext?=?sqlRouteResult.getMergeContext();?????????????????????? 7????????setMergeContext(mergeContext); 8????????setGeneratedKeyContext(sqlRouteResult.getGeneratedKeyContext()); 9????????for?(SQLExecutionUnit?each?:?sqlRouteResult.getExecutionUnits())?{??????????????????????//?@2??????????10????????????PreparedStatement?preparedStatement?=?(PreparedStatement)?getStatement(getShardingConnection().getConnection(each.getDataSource(),?sqlRouteResult.getSqlStatementType()),?each.getSql());?????//?@311????????????replayMethodsInvocation(preparedStatement);12????????????getParameters().replayMethodsInvocation(preparedStatement);13????????????result.add(wrap(preparedStatement,?each));14????????}15????????return?result;16}

代碼@1:根據SQL參數進行路由計算,本文暫不關注其具體實現細節,這些將在具體分析Sharding-jdbc時具體詳解,在這里就直觀看一下其結果:

代碼@2、@3:對分庫分表的結果進行遍歷,然后使用底層Datasource來創建Connection,創建PreparedStatement 對象。

routeSQL就暫時講到這,從這里我們得知,會在這里根據路由結果,使用底層的具體數據源創建對應的Connection與PreparedStatement 對象。

PreparedStatementExecutor#execute

1 3public?boolean?execute()?{
4????Context?context?=?MetricsContext.start("ShardingPreparedStatement-execute");
5????eventPostman.postExecutionEvents();
6????final?boolean?isExceptionThrown?=?ExecutorExceptionHandler.isExceptionThrown();
7????final?Map?dataMap?=?ExecutorDataMap.getDataMap(); 8????try?{ 9????????if?(1?==?preparedStatementExecutorWrappers.size())?{?????//?@110????????????PreparedStatementExecutorWrapper?preparedStatementExecutorWrapper?=?preparedStatementExecutorWrappers.iterator().next();11????????????return?executeInternal(preparedStatementExecutorWrapper,?isExceptionThrown,?dataMap);12????????}13????????List?result?=?executorEngine.execute(preparedStatementExecutorWrappers,?new?ExecuteUnit()?{????//?@21415????????????@Override16????????????public?Boolean?execute(final?PreparedStatementExecutorWrapper?input)?throws?Exception?{17????????????????synchronized?(input.getPreparedStatement().getConnection())?{18????????????????????return?executeInternal(input,?isExceptionThrown,?dataMap);19????????????????}20????????????}21????????});22????????return?(null?==?result?||?result.isEmpty())???false?:?result.get(0);23????}?finally?{24????????MetricsContext.stop(context);25????}26?}

代碼@1:如果計算出來的路由信息為1個,則同步執行。

代碼@2:如果計算出來的路由信息有多個,則使用線程池異步執行。

那還有一個問題,通過PreparedStatement#execute方法執行后,如何返回結果呢?特別是異步執行的。

在上文其實已經談到:

DefaultResultSetHandler#handleResultSets

1 3public?List?handleResultSets(Statement?stmt)?throws?SQLException?{
4????ErrorContext.instance().activity("handling?results").object(mappedStatement.getId());
5
6????final?List?multipleResults?=?new?ArrayList(); 7 8????int?resultSetCount?=?0; 9????ResultSetWrapper?rsw?=?getFirstResultSet(stmt);?????????//?@110????//省略部分代碼,完整代碼可以查看DefaultResultSetHandler方法。11????return?collapseSingleResultList(multipleResults);12??}1314private?ResultSetWrapper?getFirstResultSet(Statement?stmt)?throws?SQLException?{15????ResultSet?rs?=?stmt.getResultSet();??????????????//?@216????while?(rs?==?null)?{17??????//?move?forward?to?get?the?first?resultset?in?case?the?driver18??????//?doesn't?return?the?resultset?as?the?first?result?(HSQLDB?2.1)19??????if?(stmt.getMoreResults())?{20????????rs?=?stmt.getResultSet();21??????}?else?{22????????if?(stmt.getUpdateCount()?==?-1)?{23??????????//?no?more?results.?Must?be?no?resultset24??????????break;25????????}26??????}27????}28????return?rs?!=?null???new?ResultSetWrapper(rs,?configuration)?:?null;29??}

我們看一下其關鍵代碼如下:
代碼@1:調用Statement#getResultSet()方法,如果使用shardingJdbc,則會調用ShardingStatement#getResultSet(),并會處理分庫分表結果集的合并,在這里就不詳細進行介紹,該部分會在shardingjdbc專欄詳細分析。

代碼@2:jdbc statement中獲取結果集的通用寫法,這里也不過多的介紹。

mybatis shardingjdbc SQL執行流程就介紹到這里了,為了方便大家對上述流程的理解,最后給出SQL執行的流程圖:

Mybatis Sharding-Jdbc的SQL執行流程就介紹到這里了,從圖中也能清晰看到Mybatis的拆件機制,將在下文詳細介紹。

查看更多文章請關注微信公眾號:


一波廣告來襲,作者新書《RocketMQ技術內幕》已出版上市:

《RocketMQ技術內幕》已出版上市,目前可在主流購物平臺(京東、天貓等)購買,本書從源碼角度深度分析了RocketMQ NameServer、消息發送、消息存儲、消息消費、消息過濾、主從同步HA、事務消息;在實戰篇重點介紹了RocketMQ運維管理界面與當前支持的39個運維命令;并在附錄部分羅列了RocketMQ幾乎所有的配置參數。本書得到了RocketMQ創始人、阿里巴巴Messaging開源技術負責人、Linux OpenMessaging 主席的高度認可并作序推薦。目前是國內第一本成體系剖析RocketMQ的書籍。

總結

以上是生活随笔為你收集整理的statement执行insert into语句_【图文并茂】源码解析MyBatis ShardingJdbc SQL语句执行流程详解...的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。