当前位置: 首页 > news >正文

Spring cloud负载均衡 @LoadBalanced注解原理

接上一篇文章,案例代码也在上一篇文章的基础上。

在上一篇文章的案例中,我们创建了作为Eureka server的Eureka注册中心服务、作为Eureka client的userservice、orderservice。

orderservice引入RestTemplate,加入了@LoadBalanced注解,代码如下:

package com;@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {public static void main(String[] args) {SpringApplication.run(OrderServiceApplication.class);}@Bean@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}}

从而,我们实现了基于Eureka注册中心的微服务治理框架,在orderservice调用userservice的时候通过加了@LoadBalanced注解的RestTemplate实现了负载均衡。

今天的目标是:深入研究@LoadBalanced生效的底层原理。

@LoadBalanced是怎么实现负载均衡的?

我们要回答的第一个问题是,为什么@LoadBalanced能实现负载均衡?

我们从代码的源头一路追查下去…

orderservice通过RestTemplate实现对userservice的调用代码:

@Service
public class OrderService {@Autowiredprivate RestTemplate restTemplate;public String getOrder(){//通过userService获取user信息String url="http://userservice/user/getUser";System.out.println("url"+url);User user=restTemplate.getForObject(url,User.class);System.out.println(user);return user.getName();}
}

很容易的,我们需要有一个认知:这里访问的地址http://userservice/user/getUser只可能在Spring cloud服务治理环境下有意义,最终能访问到我们发布到本机上的userservice的如下服务:

1. http://localhost:8080
2. http://localhost:8081
3. http://localhost:8082

必定需要借助Spring Cloud的某一机制将http://userservice转换为上述地址之一。这个转换过程,也就是Spring Cloud的负载均衡机制。

跟踪getForObject:

	@Override@Nullablepublic <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);HttpMessageConverterExtractor<T> responseExtractor =new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);}

调用到execute方法:

	@Override@Nullablepublic <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);}

doExecute方法:

@Nullableprotected <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();}}}

createRequest方法:

	protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {ClientHttpRequest request = getRequestFactory().createRequest(url, method);initialize(request);if (logger.isDebugEnabled()) {logger.debug("HTTP " + method.name() + " " + url);}return request;}

最关键的部分来了,就是这个 getRequestFactory()方法,在RestTemplate的父类InterceptingHttpAccessor中定义:

    private final List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();public ClientHttpRequestFactory getRequestFactory() {List<ClientHttpRequestInterceptor> interceptors = getInterceptors();if (!CollectionUtils.isEmpty(interceptors)) {ClientHttpRequestFactory factory = this.interceptingRequestFactory;if (factory == null) {factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);this.interceptingRequestFactory = factory;}return factory;}else {return super.getRequestFactory();}}public List<ClientHttpRequestInterceptor> getInterceptors() {return this.interceptors;}

首先通过getInterceptor()方法检查是否有有拦截器,拦截器interceptors是由ClientHttpRequestInterceptor组成的一个list。如果有的话,就会创建InterceptingClientHttpRequestFactory、并且将拦截器interceptors送给InterceptingClientHttpRequestFactory工厂之后,返回工厂InterceptingClientHttpRequestFactory。

然后,方法调用返回到createRequest方法中,调用InterceptingClientHttpRequestFactory的createRequest方法,最终会调用到:

	@Overrideprotected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);}

方法最终返回的是InterceptingClientHttpRequest,并且,会将工厂InterceptingClientHttpRequestFactory持有的interceptors传递给InterceptingClientHttpRequest对象:

protected InterceptingClientHttpRequest(ClientHttpRequestFactory requestFactory,List<ClientHttpRequestInterceptor> interceptors, URI uri, HttpMethod method) {this.requestFactory = requestFactory;this.interceptors = interceptors;this.method = method;this.uri = uri;}

之后返回到doExecute方法中,会调用InterceptingClientHttpRequest的父类AbstractClientHttpRequest类的execute方法、之后又转回到InterceptingClientHttpRequest类的executeInternal方法:

	@Overrideprotected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();return requestExecution.execute(this, bufferedOutput);}

executeInternal方法创建了InterceptingClientHttpRequest内部类InterceptingRequestExecution对象之后,调用内部对象的execute方法。

查看内部类InterceptingRequestExecution,不难发现,他拿到了宿主类InterceptingClientHttpRequest的拦截器interceptors的迭代器:

private class InterceptingRequestExecution implements ClientHttpRequestExecution {private final Iterator<ClientHttpRequestInterceptor> iterator;public InterceptingRequestExecution() {this.iterator = interceptors.iterator();}@Overridepublic ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {if (this.iterator.hasNext()) {ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();return nextInterceptor.intercept(request, body, this);}else {HttpMethod method = request.getMethod();Assert.state(method != null, "No standard HTTP method");ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));if (body.length > 0) {if (delegate instanceof StreamingHttpOutputMessage) {StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));}else {StreamUtils.copy(body, delegate.getBody());}}return delegate.execute();}}}

然后在execute方法中首先遍历该迭代器iterator并调用迭代器对象ClientHttpRequestInterceptor的intercept方法。

拦截器ClientHttpRequestInterceptor就是实现负载均衡的关键所在,Spring正是在拦截器ClientHttpRequestInterceptor的intercept方法中,完成了负载均衡的实现:将请求中的服务名称比如本案例中的userservice、替换成了由Eureka注册中心下发下来的具体的userservice服务器(比如http://127.0.0.1:8081)

我们发现最关键的部分就是RestTemplate对象中的拦截器interceptors。

接下来的问题就是:interceptors是什么时候、怎么创建出来的?

拦截器的初始化

从RestTemplate的父类InterceptingHttpAccessor简单追踪一下,不难发现他的interceptors是通过setInterceptors方法赋值的,然后借助开发工具的帮助:
在这里插入图片描述
发现LoadBalanceAutoConfiguration调用了setInterceptors方法。

这个LoadBalanceAutoConfiguration的命名方式是不是很熟悉啊?我们前面分析过SpringBoot的自动配置,就是各种xxxxAutoConfiguration命名的类负责具体的自动配置任务的。

简单了解了下,LoadBalanceAutoConfiguration就是SpringBoot的自动配置类。我们找到LoadBalanceAutoConfiguration类在spring-cloud-commons包下,按图索骥,我们找到了包下的/META-INF/spring.factories文件:
在这里插入图片描述
因此我们知道,SpringBoot的自动配置机制会通过调用LoadBalanceAutoConfiguration类来完成LoadBalance的相关初始化工作。
所以我们接下来的工作就是要研究LoadBalanceAutoConfiguration。

LoadBalanceAutoConfiguration

第一步,从LoadBalanceAutoConfiguration类的源码知道他要通过restTemplateCustomizer方法加载一个RestTemplateCustomizer对象,方法需要一个参数LoadBalancerInterceptor:

		@Bean@ConditionalOnMissingBeanpublic RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {return restTemplate -> {List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());list.add(loadBalancerInterceptor);restTemplate.setInterceptors(list);};}

先看一眼RestTemplateCustomizer类,只有一个customize方法,该方法有一个参数RestTemplate:

public interface RestTemplateCustomizer {void customize(RestTemplate restTemplate);}

这个customize方法已经在restTemplateCustomizer方法中通过lamda表达式定义出来了,代码逻辑很简单:

就是要将LoadBalancerInterceptor拦截器对象通过调用RestTemplate的setInterceptors方法加入到RestTemplate的interceptors中!

似乎快要摸到开关了!!!

那么我们现在又冒出了以下几个问题:

  1. 这个LoadBalancerInterceptor从哪里来?
  2. RestTemplate从哪里来?
  3. 什么时候调用这个已经注入到Spring Ioc容器中的RestTemplateCustomizer的customize方法?

第1、第2两个问题其实很简单。看代码:

    @Configuration(proxyBeanMethods = false)@Conditional(RetryMissingOrDisabledCondition.class)static class LoadBalancerInterceptorConfig {@Beanpublic LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient,LoadBalancerRequestFactory requestFactory) {return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);}...
}

LoadBalancerAutoConfiguration类中有一个静态配置类LoadBalancerInterceptorConfig,通过loadBalancerInterceptor方法注入了LoadBalancerInterceptor 对象,创建对象的时候通过参数注入了loadBalancerClient和LoadBalancerRequestFactory。

第2个问题,RestTemplate的注入,其实是我们从应用层通过@Bean注入的,同时加了@LoadBalanced注解。

现在我们来回答第3个问题:

什么时候调用这个已经注入到Spring Ioc容器中的RestTemplateCustomizer的customize方法?

比前两个问题稍稍复杂了一点:

	@LoadBalanced@Autowired(required = false)private List<RestTemplate> restTemplates = Collections.emptyList();@Beanpublic SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {return () -> restTemplateCustomizers.ifAvailable(customizers -> {for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {for (RestTemplateCustomizer customizer : customizers) {customizer.customize(restTemplate);}}});}

首先看到标注了@Autowire和@LoadBalanced注解的RestTemplates列表的注入,意思是: 如果有加了@LoadBalanced注解的RestTemplates bean的话,就自动装配到restTemplates 变量中。

之后,通过loadBalancedRestTemplateInitializerDeprecated方法注入了一个SmartInitializingSingleton,我们知道SmartInitializingSingleton在装配到Spring Ioc之后会调用他的afterSingletonsInstantiated()方法。这里注入的SmartInitializingSingleton通过lamda实现了afterSingletonsInstantiated()方法,代码逻辑:通过方法参数打包注入到Ioc容器中的RestTemplateCustomizer(前面讲过了,他已经注入到IoC容器中了)到restTemplateCustomizers中,然后遍历restTemplates(这个list在前面说过了,已经把我们通过@Bean和@LoadBalanced注解的RestTemplate对象注入进来了)、针对每一个RestTemplate再遍历restTemplateCustomizers中的RestTemplateCustomizer对象,逐个调用他的customize方法。

OK!对于@LoadBalanced注解从应用、到初始化、生效机制,我们就分析清楚了。

最后还遗留两个小问题,初始化和应用两端各一个:初始化过程中的装配到loadBalancerInterceptor对象中的LoadBalancerClient具体是什么对象、什么时候注入的?应用端最终的负载均衡策略、负载均衡实现逻辑,我们还没具体分析。

下一篇文章解决上述两个问题。

相关文章:

Spring cloud负载均衡 @LoadBalanced注解原理

接上一篇文章&#xff0c;案例代码也在上一篇文章的基础上。 在上一篇文章的案例中&#xff0c;我们创建了作为Eureka server的Eureka注册中心服务、作为Eureka client的userservice、orderservice。 orderservice引入RestTemplate&#xff0c;加入了LoadBalanced注解&#x…...

C#when关键字

在C#中&#xff0c;when关键字用于在模式匹配表达式中添加条件。它允许您在模式匹配的过程中指定额外的条件&#xff0c;以进一步过滤匹配的模式。当模式匹配和附加条件都为真时&#xff0c;相关的代码块将被执行。 以下是when关键字的详细解释以及示例说明&#xff1a; 语法…...

华为政企无线局域网产品集

产品类型产品型号产品说明 室内接入点AirEngine 5760-51AirEngine 5760-51是华为发布的支持Wi-Fi 6&#xff08;802.11ax&#xff09;标准的新一代室内AP&#xff0c;适合部署在企业办公、零售、制造等场景。 通过软件定义射频&#xff0c;能够在双频、三频模式灵活切换&a…...

解释 RESTful API

RESTful API是一种基于HTTP协议的API设计风格&#xff0c;它的核心思想是将每个资源&#xff08;如用户、订单等&#xff09;抽象成一个URI&#xff08;统一资源标识符&#xff09;&#xff0c;通过HTTP协议定义的方法&#xff08;如GET、POST、PUT、DELETE等&#xff09;对资源…...

青翼科技-国产化ARM系列TES720D-KIT

板卡概述 TES720D-KIT是专门针对我司TES720D&#xff08;基于复旦微FMQL20S400的全国产化ARM核心板&#xff09;的一套开发套件&#xff0c;它包含1个TES720D核心板&#xff0c;加上一个TES720D-EXT扩展底板。 FMQL20S400是复旦微电子研制的全可编程融合芯片&#xff0c;在单…...

Tomcat为什么支持线程池?

Tomcat作为一个Java Servlet容器&#xff0c;支持线程池是因为它能够处理多个并发请求。这些请求可以是对Web应用程序的HTTP请求、Servlet的请求&#xff0c;或其他支持的协议。 支持线程池的主要原因包括&#xff1a; 并发处理能力&#xff1a; 提高性能&#xff1a; 使用线程…...

Mac安装VMware

去官网下载一下VMware Download VMware Fusion | VMware | SG 下载完成之后&#xff0c;打开直接闪退&#xff0c;参考这篇文章解决 解决macOS13安装Fusion13闪退的问题-CSDN博客 然后即可成功顺行...

项目部署文档

申请SSL证书 先申请,用免费的 下载证书 先将下载下来的保存起来 服务器安装JDK: 创建develop目录 mkdir /usr/local/develop/ 把JDK压缩包上传到/usr/local/develop/目录 解压安装包 并且将安装到指定目录 tar -zxvf /usr/local/develop/jdk-8u191-linux-x64.tar.gz -C /us…...

HTML+CSS阶段知识点梳理

目录 一、简单的网页结构 二、常用标签 三、列表 四、CSS引入方式 五、常用选择器 1、标签&#xff08;元素&#xff09;选择器 2、id选择器 3、class选择器 4、通配选择器 5、复合选择器 6、关系选择器 7、属性选择器 8、伪类选择器 9、a元素的伪类 10、伪元素…...

网易按照作者批量采集新闻资讯软件说明文档

大家好&#xff0c;我是淘小白~ 今天给大家介绍的爬虫软件是网易按照作者采集的软件 1、软件语言&#xff1a; Python 2、使用到的工具 Python selenium库、谷歌浏览器、谷歌浏览器驱动 3、文件说明&#xff1a; 4、配置文件说明&#xff1a; 5、环境配置 安装Python&am…...

SwiftUI 代码调试之都是“变心”惹的祸

0. 概览 这是一段非常简单的 SwiftUI 代码&#xff0c;我们将 Item 数组传递到子视图并在子视图中对其进行修改&#xff0c;修改的结果会立即在主视图中反映出来。 不幸的是&#xff0c;当我们修改 Item 名称时却发现不能连续输入&#xff1a;每次敲一个字符键盘都会立即收起并…...

u20.04安装slam库

git clone https://github.com/strasdat/Sophus.git // 下载的最新版是模板类的 git checkout a621ff // 切换为非模板类的历史版本 模板类Sophus的依赖库是Eigen(版本为3.3.X)和fmt&#xff0c;需提前安装好Eigen库和fmt库 git clone https://github.c…...

齐纳二极管,肖特基二极管,瞬态电压抑制二极管

普通二极管&#xff0c;齐纳二极管&#xff0c;肖特基二极管的符号&#xff1a; 瞬态电压抑制&#xff08;TVS&#xff09;二极管是一种特殊的齐纳二极管&#xff0c;其符号如下&#xff1a; 普通二极管 普通二极管由n类型 的半导体和p类型的半导体结合而成。 硅材料制成的二…...

axios 全局错误处理和请求取消

这两个功能都是用拦截器实现。 前景提要&#xff1a; ts 简易封装 axios&#xff0c;统一 API 实现在 config 中配置开关拦截器 全局错误处理 在构造函数中&#xff0c;添加一个响应拦截器即可。在构造函数中注册拦截器的好处是&#xff0c;无论怎么实例化封装类&#xff0c…...

无法加载文件 C:\Program Files\nodejs\cnpm.ps1,因为在此系统上禁止运行脚本。有

cnpm : 无法加载文件 C:\Program Files\nodejs\cnpm.ps1&#xff0c;因为在此系统上禁止运行脚本。有关详细信息&#xff0c;请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Poli cies。 所在位置 行:1 字符: 1 cnpm run debug ~~~~ Categ…...

学电脑编程零基础,计算机编程入门先学什么

学电脑编程零基础&#xff0c;计算机编程入门先学什么&#xff0c;建议先从容易学习的语言入手&#xff0c;比如中文编程。 给大家分享一款中文编程工具&#xff0c;零基础轻松学编程&#xff0c;不需英语基础&#xff0c;编程工具可下载。 这款工具不但可以连接部分硬件&…...

SQL左连接实战案例

要求&#xff1a;用表df1和表df2的数据&#xff0c;得到df3 一、创建表 CREATE TABLE df1 (姓名 varchar(255) DEFAULT NULL,年龄 int DEFAULT NULL,部门 varchar(255) DEFAULT NULL,id int DEFAULT NULL );CREATE TABLE df2 (部门 varchar(255) DEFAULT NULL,年龄 int DEFAU…...

2、Sentinel基本应用限流规则(2)

2.2.1 是什么 Sentinel 是阿里中间件团队开源的&#xff0c;面向分布式服务架构的轻量级高可用流量控制组件&#xff0c;主要以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。 2.2.2 基本概念 • 资源 (需要被保护的东西…...

Qt的事件

2023年11月5日&#xff0c;周日上午 还没写完&#xff0c;不定期更新 目录 事件处理函数的字体特点Qt事件处理的工作原理一些常用的事件处理函数Qt中的事件类型QEvent类的type成员函数可以用来判断事件的类型事件的类型有哪些&#xff1f;有多少种事件类 事件处理函数的字体特…...

MTK联发科天玑9000旗舰5G移动平台处理器_MT6983芯片定制开发

MT6983天玑9000采用台积电4纳米工艺制程&#xff0c;CPU采用“134”三丛集Armv9架构&#xff0c;APU性能提升&#xff0c;ISP处理速度提升&#xff0c;最高支持3.2亿像素摄像头&#xff0c;采用Mali-G710十核GPU&#xff0c;搭载R16 5G调制解调器。 MT6983天玑9000芯片基本概…...

大话软工笔记—需求分析概述

需求分析&#xff0c;就是要对需求调研收集到的资料信息逐个地进行拆分、研究&#xff0c;从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要&#xff0c;后续设计的依据主要来自于需求分析的成果&#xff0c;包括: 项目的目的…...

系统设计 --- MongoDB亿级数据查询优化策略

系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log&#xff0c;共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题&#xff0c;不能使用ELK只能使用…...

04-初识css

一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf

FTP 客服管理系统 实现kefu123登录&#xff0c;不允许匿名访问&#xff0c;kefu只能访问/data/kefu目录&#xff0c;不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

腾讯云V3签名

想要接入腾讯云的Api&#xff0c;必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口&#xff0c;但总是卡在签名这一步&#xff0c;最后放弃选择SDK&#xff0c;这次终于自己代码实现。 可能腾讯云翻新了接口文档&#xff0c;现在阅读起来&#xff0c;清晰了很多&…...

Ubuntu系统复制(U盘-电脑硬盘)

所需环境 电脑自带硬盘&#xff1a;1块 (1T) U盘1&#xff1a;Ubuntu系统引导盘&#xff08;用于“U盘2”复制到“电脑自带硬盘”&#xff09; U盘2&#xff1a;Ubuntu系统盘&#xff08;1T&#xff0c;用于被复制&#xff09; &#xff01;&#xff01;&#xff01;建议“电脑…...

SpringAI实战:ChatModel智能对话全解

一、引言&#xff1a;Spring AI 与 Chat Model 的核心价值 &#x1f680; 在 Java 生态中集成大模型能力&#xff0c;Spring AI 提供了高效的解决方案 &#x1f916;。其中 Chat Model 作为核心交互组件&#xff0c;通过标准化接口简化了与大语言模型&#xff08;LLM&#xff0…...

6.9-QT模拟计算器

源码: 头文件: widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QMouseEvent>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nullptr);…...

ArcPy扩展模块的使用(3)

管理工程项目 arcpy.mp模块允许用户管理布局、地图、报表、文件夹连接、视图等工程项目。例如&#xff0c;可以更新、修复或替换图层数据源&#xff0c;修改图层的符号系统&#xff0c;甚至自动在线执行共享要托管在组织中的工程项。 以下代码展示了如何更新图层的数据源&…...