如何正确理解RestTemplate远程调用的实现原理?

news2024/12/23 9:45:32

本文从源码出发理解RestTemplate实现远程调用的底层原理。

初始化RestTemplate实例

我们可以通过RestTemplate所提供的几个构造函数来对其进行初始化。在分析这些构造函数之前,有必要先看一下RestTemplate类的定义,如下所示:

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations

可以看到,RestTemplate扩展了InterceptingHttpAccessor抽象类,并实现了RestOperations接口。我们围绕RestTemplate的方法定义来梳理它在设计上的思想。

首先,我们来到RestOperations接口的定义,这里截取了部分核心方法,如下所示:

public interface RestOperations {

<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;

<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;

<T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables) throws RestClientException;

void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException;

void delete(String url, Object... uriVariables) throws RestClientException;

<T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity,

Class<T> responseType, Object... uriVariables) throws RestClientException;

}

显然,正是RestOperations接口定义了所有我们上一课时中介绍到的get/post/put/delete/exhange等远程调用方法组,而这些方法都是遵循RESTful架构风格而设计的。RestTemplate对这些接口都提供了实现,这是它的一条代码支线。

然后,我们再来看InterceptingHttpAccessor,它是一个抽象类,包含的核心变量如下所示:

public abstract class InterceptingHttpAccessor extends HttpAccessor {

private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();

private volatile ClientHttpRequestFactory interceptingRequestFactory;

}

通过变量定义,我们明确了InterceptingHttpAccessor应该包含两部分的处理功能,一部分是设置和管理请求拦截器ClientHttpRequestInterceptor,另一部分则是负责获取用于创建客户端HTTP请求的工厂类ClientHttpRequestFactory。

同时,我们注意到InterceptingHttpAccessor同样存在一个父类HttpAccessor,这个父类值得展开一下,因为它真正完成了ClientHttpRequestFactory 创建以及如何通过ClientHttpRequestFactory获取代表客户端请求的ClientHttpRequest对象。HttpAccessor的核心变量如下所示:

public abstract class HttpAccessor {

private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

}

可以看到,HttpAccessor中创建了SimpleClientHttpRequestFactory作为系统默认的ClientHttpRequestFactory。关于ClientHttpRequestFactory,本课时后续内容中还会进行详细的讨论、

作为总结,我们来梳理一下RestTemplate的类层结构,如下所示:


从RestTemplate的类层结构中,我们可以理解它的设计思想。整个类层结构可以清晰的分成两条支线,左边部分用于完成与HTTP请求相关的实现机制,而后边部分则提供了基于RESTful风格的操作入口,并使用了面向对象中的接口和抽象类完成了这两部分功能的聚合。

介绍完RestTemplate的实例化过程,接下来我们来分析它的核心执行流程。作为用于远程调用的模板工具类,我们可以从具备多种请求方式的exchange方法入手,该方法如下所示:

@Override

public <T> ResponseEntity<T> exchange(String url, HttpMethod method,

@Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)

throws RestClientException {

//构建请求回调

RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType);

//构建响应体抽取器

ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);

//执行远程调用

return nonNull(execute(url, method, requestCallback, responseExtractor, uriVariables));

}

显然,我们应该进一步关注这里的execute方法。事实上,无论我们采用get/put/post/delete中的哪种方法来发起请求,在RestTemplate负责执行远程调用的都是这个execute方法,该方法定义如下所示:

@Override

@Nullable

public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {

URI expanded = getUriTemplateHandler().expand(url, uriVariables);

return doExecute(expanded, method, requestCallback, responseExtractor);

}

execute方法首先通过UriTemplateHandler构建了一个URI,然后将请求过程委托给了doExecute方法进行处理,该方法定义如下:

protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,

@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

Assert.notNull(url, "URI is required");

Assert.notNull(method, "HttpMethod is required");

ClientHttpResponse response = null;

try {

//创建请求对象

ClientHttpRequest request = createRequest(url, method);

if (requestCallback != null) {

//执行对请求的回调

requestCallback.doWithRequest(request);

}

//获取调用结果

response = request.execute();

//处理调用结果

handleResponse(url, method, response);

//使用结果提取从结果中提取数据

return (responseExtractor != null ? responseExtractor.extractData(response) : null);

}

catch (IOException ex) {

String resource = url.toString();

String query = url.getRawQuery();

resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);

throw new ResourceAccessException("I/O error on " + method.name() +

" request for \"" + resource + "\": " + ex.getMessage(), ex);

}

finally {

if (response != null) {

response.close();

}

}

}

从上述方法中,我们可以清晰地看到使用RestTemplate进行远程调用所涉及到的三大步骤,即创建请求对象、执行远程调用以及处理响应结果。让我们一起来分别来看一下。

创建请求对象

创建请求对象的入口方法如下所示:

ClientHttpRequest request = createRequest(url, method);

跟进这里的createRequest方法,我们发现流程就执行到了前面介绍的HttpAccessor类,如下所示:

public abstract class HttpAccessor {

private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {

ClientHttpRequest request = getRequestFactory().createRequest(url, method);

if (logger.isDebugEnabled()) {

logger.debug("Created " + method.name() + " request for \"" + url + "\"");

}

return request;

}

}

创建ClientHttpRequest的过程是一种典型的工厂模式应用场景,这里直接创建了一个实现ClientHttpRequestFactory 接口的SimpleClientHttpRequestFactory对象,然后再通过这个对象的createRequest方法创建了客户端请求对象ClientHttpRequest并返回给上层组件进行使用。ClientHttpRequestFactory接口的定义如下所示:

public interface ClientHttpRequestFactory {

//创建客户端请求对象

ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException;

}

在Spring中,存在一批ClientHttpRequestFactory 接口的实现类,SimpleClientHttpRequestFactory是它的默认实现,开发人员也可以根据需要实现自定义的ClientHttpRequestFactory。简单起见,我们直接跟踪SimpleClientHttpRequestFactory的代码,来到它的createRequest方法,如下所示:

private boolean bufferRequestBody = true;

@Override

public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {

HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);

prepareConnection(connection, httpMethod.name());

if (this.bufferRequestBody) {

return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);

}

else {

return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);

}

}

上述createRequest中,首先我们通过传入的URI对象构建了一个HttpURLConnection对象,然后对该对象进行一些预处理,最后构造并返回一个ClientHttpRequest的实例。

通过翻阅代码,我们发现在上述openConnection方法中就是简单地通过URL对象的openConnection方法返回了一个UrlConnection。而在prepareConnection方法中,也只是完成了对HttpUrlConnection超时时间、请求方法等常见属性的设置。

注意到bufferRequestBody参数的值为true,所以通过createRequest方法最终结果是返回一个SimpleBufferingClientHttpRequest对象。

执行远程调用

一旦获取请求对象,就可以发起远程调用并获取响应了,RestTemplate中的入口方法如下所示:

response = request.execute();

这里的request就是前面创建的SimpleBufferingClientHttpRequest类,我们可以先来看一下该类的类层结构,如下图所示:


在上图的AbstractClientHttpRequest中,定义了如下所示的execute方法:

@Override

public final ClientHttpResponse execute() throws IOException {

assertNotExecuted();

ClientHttpResponse result = executeInternal(this.headers);

this.executed = true;

return result;

}

protected abstract ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException;

AbstractClientHttpRequest类的作用就就是防止HTTP请求的Header和Body被多次写入,所以在这个execute方法返回之前设置了executed标志位。同时,在execute方法中,最终调用了一个抽象方法executeInternal,而这个方法的实现是在AbstractClientHttpRequest的子类AbstractBufferingClientHttpRequest中,如下所示:

@Override

protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {

byte[] bytes = this.bufferedOutput.toByteArray();

if (headers.getContentLength() < 0) {

headers.setContentLength(bytes.length);

}

ClientHttpResponse result = executeInternal(headers, bytes);

this.bufferedOutput = new ByteArrayOutputStream(0);

return result;

}

protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException;

和AbstractClientHttpRequest类一样,这里进一步梳理了一个抽象方法executeInternal,而这个抽象方法则由最底层的SimpleBufferingClientHttpRequest类来实现,如下所示:

@Override

protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {

addHeaders(this.connection, headers);

// JDK <1.8 doesn't support getOutputStream with HTTP DELETE

if (getMethod() == HttpMethod.DELETE && bufferedOutput.length == 0) {

this.connection.setDoOutput(false);

}

if (this.connection.getDoOutput() && this.outputStreaming) {

this.connection.setFixedLengthStreamingMode(bufferedOutput.length);

}

this.connection.connect();

if (this.connection.getDoOutput()) {

FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());

}

else {

// Immediately trigger the request in a no-output scenario as well

this.connection.getResponseCode();

}

return new SimpleClientHttpResponse(this.connection);

}

这里通过FileCopyUtils.copy工具方法将结果写入到输出流上。而executeInternal方法最终返回的是一个包装了Connection对象的SimpleClientHttpResponse。

处理响应结果

一个HTTP请求处理的最后一步就是从ClientHttpResponse中读取输入流,格式化成一个响应体并将其转化为业务对象,入口代码如下所示:

//处理调用结果

handleResponse(url, method, response);

//使用结果提取从结果中提取数据

return (responseExtractor != null ? responseExtractor.extractData(response) : null);

我们先来看这里的handleResponse方法,定义如下:

protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {

ResponseErrorHandler errorHandler = getErrorHandler();

boolean hasError = errorHandler.hasError(response);

if (logger.isDebugEnabled()) {

try {

logger.debug(method.name() + " request for \"" + url + "\" resulted in " +

response.getRawStatusCode() + " (" + response.getStatusText() + ")" +

(hasError ? "; invoking error handler" : ""));

}

catch (IOException ex) {

// ignore

}

}

if (hasError) {

errorHandler.handleError(url, method, response);

}

}

这段代码实际上并没有真正处理返回的数据,而只是执行了错误处理。通过getErrorHandler方法获取了一个ResponseErrorHandler,如果响应的状态码是错误的,那么就调用handleError处理错误并抛出异常。

那么,获取响应数据并完成转化的工作应该是在ResponseExtractor中,该接口定义如下所示:

public interface ResponseExtractor<T> {

@Nullable

T extractData(ClientHttpResponse response) throws IOException;

}

在RestTemplate类中,定义了一个ResponseEntityResponseExtractor内部类来实现了ResponseExtractor接口,如下所示:

private class ResponseEntityResponseExtractor <T> implements ResponseExtractor<ResponseEntity<T>> {

@Nullable

private final HttpMessageConverterExtractor<T> delegate;

public ResponseEntityResponseExtractor(@Nullable Type responseType) {

if (responseType != null && Void.class != responseType) {

this.delegate = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);

}

else {

this.delegate = null;

}

}

@Override

public ResponseEntity<T> extractData(ClientHttpResponse response) throws IOException {

if (this.delegate != null) {

T body = this.delegate.extractData(response);

return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).body(body);

}

else {

return ResponseEntity.status(response.getRawStatusCode()).headers(response.getHeaders()).build();

}

}

}

可以看到,ResponseEntityResponseExtractor中的extractData方法本质上是将数据提取部分的工作委托给了一个代理对象delegate,而这个delegate的类型就是HttpMessageConverterExtractor。从命名上看,我们不难想象,在HttpMessageConverterExtractor类的内部,肯定是使用了上一课时所介绍的HttpMessageConverter来完成消息的转换,如下所示(代码做了裁剪):

public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {

private final List<HttpMessageConverter<?>> messageConverters;

@Override

@SuppressWarnings({"unchecked", "rawtypes", "resource"})

public T extractData(ClientHttpResponse response) throws IOException {

MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);

if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {

return null;

}

MediaType contentType = getContentType(responseWrapper);

try {

for (HttpMessageConverter<?> messageConverter : this.messageConverters) {

if (messageConverter instanceof GenericHttpMessageConverter) {

GenericHttpMessageConverter<?> genericMessageConverter =

(GenericHttpMessageConverter<?>) messageConverter;

if (genericMessageConverter.canRead(this.responseType, null, contentType)) {

return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);

}

}

if (this.responseClass != null) {

if (messageConverter.canRead(this.responseClass, contentType)) {

return (T) messageConverter.read((Class) this.responseClass, responseWrapper);

}

}

}

}

}

上述方法看上去有点复杂,但核心逻辑就是遍历HttpMessageConveter列表,然后判断其是否能够读取数据,如果能就调用read方法读取数据。

最后,我们来讨论一下HttpMessageConveter中的这个read方法是如何实现的。让我们来到HttpMessageConveter接口的抽象实现类AbstractHttpMessageConverter,在它的read方法中同样定义了一个抽象方法readInternal,如下所示:

@Override

public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage)  throws IOException, HttpMessageNotReadableException {

return readInternal(clazz, inputMessage);

}

protected abstract T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

在上一课时中,我们已经提到Spring提供了一系列的HttpMessageConveter来完成消息的转换,这里面最简单的应该就是StringHttpMessageConverter,该类的read方法如下所示:

@Override

protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {

Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());

return StreamUtils.copyToString(inputMessage.getBody(), charset);

}

StringHttpMessageConverter的实现过程就是从输入消息HttpInputMessage中通过getBody方法获取消息体,也就是一个ClientHttpResponse对象,然后再通过copyToString方法从该对象中读取数据,并返回字符串结果。

至此,通过RestTemplate发起、执行以及响应整个HTTP请求的完整流程就介绍完毕了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1652052.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OC5864 0.6A输出 60V输入 500KHZ DCDC降压转换IC

一级代理 技术支持 提供样品测试 Tel&#xff1a;18028786817 简介 OC5864是一款内置功率MOSFET的单片降压型开关模式转换器。OC5864在5.5~60V宽输入电源范围内实现0.6A峰值输出电流&#xff0c;并且具有出色的线电压和负载调整率。 OC5864采用PWM电流模工作模式&#xff0c;…

技术分享 | i.MX8M Mini适配MIPI转eDP芯片

1.方案概述 此方案使用HD-8MMN-CORE的核心板搭配TI公司的芯片SN65DSI86转换芯片实现。 SN65DSI86作为一款MIPI DSI转eDP的芯片&#xff0c;支持双通道DSI输入&#xff0c;最大四通道显示输出&#xff0c;最大支持4K60fps输出&#xff0c;WUXGA 1080P。本方案中将采用单通道DS…

Vue从入门到实战Day01

一、Vue快速上手 1. vue概念 概念&#xff1a;Vue是一个用于 构建用户界面的 渐进式 框架 构建用户界面&#xff1a;基于数据动态渲染页面渐进式&#xff1a;循序渐进的学习框架&#xff1a;一套完整的项目解决方案&#xff0c;提升开发效率 优点&#xff1a;大大提升开发效…

[SWPUCTF 2021 新生赛]PseudoProtocols、[SWPUCTF 2022 新生赛]ez_ez_php

[SWPUCTF 2021 新生赛]PseudoProtocols 打开环境&#xff0c;提示hint.php就在这里&#xff0c;且含有参数wllm 尝试利用PHP伪协议读取该文件 ?wllmphp://filter/convert.base64-encode/resourcehint.php//文件路径php://filter 读取源代码并进行base64编码输出。 有一些敏…

哪款充电宝质量和口碑比较好?适合入手充电宝有哪些?

充电宝这么好用的移动电源就不用我说了吧&#xff0c;平时不出门还好&#xff0c;家里有插座可以充电&#xff0c;但是但凡出门了&#xff0c;手机电量一般是不能够支撑到&#xff0c;像我这种手机重度使用者&#xff0c;出门在外手机快没电了我就非常焦虑了&#xff0c;有一款…

在VMware上利用Samba实现资源共享

一、背景 FTP协议能让主机之间的文件传输变得简单方便&#xff0c;但是FTP协议的本质是传输文件。 举个栗子&#xff1a;当客户端想修改服务器上的test.txt&#xff0c;需要先get test.txt将文件下载下来&#xff0c;修改后再put test.txt 有没有一种方式能使客户端直接修改…

Tansformer原理解读

什么是注意力机制 生物学中的注意力机制是指人类或动物能够选择性地将感知和认知资源集中到某些信息或任务上的能力。这种能力允许我们在众多信息的背景中过滤出重要的信息&#xff0c;并将这些信息传递给相应的神经元进行处理。 本质&#xff1a;能从中抓住重点&#xff0c;…

“芯”心相“蜥” 共筑未来!龙蜥社区走进兆芯 MeetUp 圆满结束

4 月 26 日&#xff0c;以“芯”心相“蜥” 共筑未来 为主题的龙蜥社区「走进系列」之走进兆芯 MeetUp 在上海成功召开。来自统信软件、联和东海、众新科技等企业参会代表共聚兆芯&#xff0c;介绍了当前企业产品和技术创新等方面的最新成果&#xff0c;并围绕社区发展建设、行…

【linux】dmesg工具

dmesg介绍 dmesg工具用途&#xff1a; dmesg - print or control the kernel ring buffer kernel ring buffer, 内核环形缓冲区&#xff0c;也叫环形队列&#xff0c;Linux内核日志就存储在一个环形队列中&#xff0c;环形队列满的时候&#xff0c;新的消息会覆盖掉旧的消息。…

如何实现“人岗匹配”?华恒智信人力资源咨询公司这样做

【客户评价 为了合理有效地评价员工&#xff0c;真正实现员工的“人岗匹配”&#xff0c;我公司提出了对公司人员展开人才测评的项目需求。经过对多家咨询公司的仔细挑选&#xff0c;我们最终选择了专业的人力资源咨询公司——华恒智信作为我们的项目合作伙伴。 在项目咨询中&a…

内网穿透使用教程

什么是内网穿透 内网穿透&#xff0c;即NAT穿透&#xff0c;网络连接时术语&#xff0c;计算机是局域网内时&#xff0c;外网与内网的计算机节点需要连接通信&#xff0c;有时就会出现不支持内网穿透。就是说映射端口&#xff0c;能让外网的电脑找到处于内网的电脑&#xff0c…

Star15.3k,开源数据可视化分析工具项目

好东西来了&#xff0c;这是一个人人可用的开源数据可视化分析工具项目&#xff0c;V 哥迫不及待的要给大家推荐这个项目&#xff0c;帆软、Tableau 等商业 BI 工具的开源替代&#xff0c;已在 Github 上被 Star了15.3k了&#xff0c;大家一起来了解一下。自己搭建起来可用&…

酷开科技 |酷开系统,给家里多点乐趣~

作为家庭娱乐的核心枢纽&#xff0c;酷开系统致力于为每一个家庭带来更多的乐趣和欢笑。通过其智能化的设计和个性化的服务&#xff0c;酷开系统正在逐渐改变家庭娱乐的方式&#xff0c;让客厅成为家中温馨的娱乐中心。 首先&#xff0c;酷开系统的界面友好而直观&#xff0c;…

【SSL证书】免费单域名SSL证书怎么申请

1、访问证书颁发机构&#xff08;CA&#xff09;&#xff1a;比如JoySSL、ZeroSSL、各大云平台等&#xff0c;这些机构提供免费的SSL证书申请&#xff0c;并支持单域名证书。在网站上注册账号&#xff0c;并根据提示选择单域名证书进行申请。 &#xff01;&#xff01;&#xf…

Foxmail邮箱API发送邮件失败的原因有哪些?

Foxmail邮箱API发送邮件的注意事项&#xff1f;如何用API发信&#xff1f; 在使用Foxmail邮箱API发送邮件时&#xff0c;有时会遇到发送失败的情况。这种情况可能由多种原因造成&#xff0c;下面AokSend就来详细探讨一下Foxmail邮箱API发送邮件失败的可能原因。 Foxmail邮箱A…

nginx--tcp负载均衡

mysql负载均衡 安装mysql yum install -y mariadb-server systemctl start mariadb systemctl enable mariadb ss -ntl创建数据库并授权 MariaDB [(none)]> create database wordpress; Query OK, 1 row affected (0.00 sec)MariaDB [(none)]> grant all privileges o…

使用 Docker 部署 TaleBook 私人书籍管理系统

1&#xff09;项目介绍 GitHub&#xff1a;https://github.com/talebook/talebook Talebook 是一个简洁但强大的私人书籍管理系统。它基于 Calibre 项目构建&#xff0c;具备书籍管理、在线阅读与推送、用户管理、SSO 登录、从百度/豆瓣拉取书籍信息等功能。 友情提醒&#x…

案例解析 | 金融行业12项个人信息违规场景及合规要点披露!

2024年4月&#xff0c;国家金融监督管理总局重庆监管局、重庆市地方金融管理局、重庆市通信管理局三部门联合印发《关于促进金融服务类App个人信息保护合规经营能力提升的通知》&#xff08;以下简称《通知》&#xff09;。此前工信体系从未发布针对金融服务类App的针对性指引文…

Golang | Leetcode Golang题解之第74题搜索二维矩阵

题目&#xff1a; 题解&#xff1a; func searchMatrix(matrix [][]int, target int) bool {m, n : len(matrix), len(matrix[0])i : sort.Search(m*n, func(i int) bool { return matrix[i/n][i%n] > target })return i < m*n && matrix[i/n][i%n] target }

结构方程模型【SEM】:系统发育数据纳入结构方程模型技术

张老师&#xff08;研究员&#xff09;&#xff0c;长期从事R语言结构方程模型、群落生态学、保护生物学、景观生态学和生态模型方面的研究和教学工作&#xff0c;已发表了多篇论文&#xff0c;拥有丰富的科研及实践经验。 对于包含物种信息的数据而言&#xff0c;物种的亲缘关…