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

spring cloud gateway集成sentinel并扩展支持restful api进行url粒度的流量治理

sentinel集成网关支持restful接口进行url粒度的流量治理

  • 前言
  • 使用网关进行总体流量治理(sentinel版本:1.8.6)
    • 1、cloud gateway添加依赖:
    • 2、sentinel配置
    • 3、网关类型项目配置
    • 4、通过zk事件监听刷新上报api分组信息
      • 1、非网关项目上报api分组信息
      • 2、网关添加监听事件
      • 3、网关监听事件处理
    • 5、sentinel控制台启动

前言

sentinel作为开源的微服务、流量治理组件,在对restful接口的支持上,在1.7之后才开始友好起来,对于带有@PathVariable的restful接口未作支持,在sentinel中/api/{id}这样的接口,其中/api/1与/api/2会被当做两个不同的接口处理,因此很难去做类似接口的流量治理,但在之后,sentinel团队已经提供了响应的csp扩展依赖,下文将会逐步讲述如何通过sentinel扩展来支持相应的服务流量治理

使用网关进行总体流量治理(sentinel版本:1.8.6)

这里选型为spring cloud gateway,而sentinel也对spring cloud gateway做了特殊照顾

1、cloud gateway添加依赖:

 <!-- alibaba封装的sentinel的starter --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId><version>2021.1</version></dependency><!-- alibaba封装的sentinel支持网关的starter --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId><version>2021.1</version></dependency><!-- 此包即为sentinel提供的扩展支持restful接口的依赖 --><dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-spring-webmvc-adapter</artifactId><version>1.8.0</version></dependency>

上述需要重点关注的是sentinel-spring-webmvc-adapter包,此依赖是支持restful接口的关键,不需要我们自己改造。

2、sentinel配置

spring:cloud:sentinel:transport:#sentinel控制台地址dashboard: 1.1.1.1:8600#sentinel通信端口,默认为8179,被占用会继续扫描,一般固定起来port: 8700#项目所在服务器ipclient-ip: 2.2.2.2#心跳启动eager: true

client-ip在某些情况下不配置会出现sentinl控制台页面只有首页,服务一直注册不上去的情况,如果出现这种情况一定要配置上,如果没有这种情况,client-IP可以不配置,同时上述配置的这些ip端口都需要连通。

3、网关类型项目配置

/*** @classDesc:* @author: cyjer* @date: 2023/1/30 9:53*/
@SpringBootApplication
@EnableCaching
@Slf4j
public class SiriusApplication {public static void main(String[] args) {System.getProperties().setProperty("csp.sentinel.app.type", "1");SpringApplication.run(SiriusApplication.class, args);log.info("<<<<<<<<<<启动成功>>>>>>>>>>");}}

如果是网关类型的项目,需要配置csp.sentinel.app.type= 1,普通项目与网关项目,在控制台上所展示和可使用的功能是不同的

4、通过zk事件监听刷新上报api分组信息

通过将接口分组按照不同粒度,如controller粒度,和具体api接口粒度,通过zookeeper修改数据监听的方式,通过网关监听该事件,实现将api分组信息写入到sentinel中。

1、非网关项目上报api分组信息

/*** @classDesc: 扫描项目接口上报api* @author: cyjer* @date: 2023/2/10 13:46*/
@Configuration
@Slf4j
@Order(1)
@RequiredArgsConstructor
public class ApiDefinitionReporter implements BeanPostProcessor, CommandLineRunner, Constraint {private final List<ApiSiriusDefinition> apiSiriusDefinitionList = new ArrayList<>();private final GatewayServiceProperties gatewayServiceProperties;private final Environment environment;private final static char JTR = '/';private final static String PASS = "/**";private final static String APPLICATION_NAME = "spring.application.name";private final static String CONTEXT_PATH = "server.servlet.context-path";private final static List<String> PASS_LIST = Arrays.asList("swaggerWelcome", "basicErrorController", "swaggerConfigResource", "openApiResource");private final ZookeeperService zookeeperService;@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {// url访问路径为:访问基地址basePath+classMappingPath+methodPathif (!gatewayServiceProperties.isAutoReportAndRegister() || PASS_LIST.contains(beanName)) {return bean;}Class<?> beanClass = bean.getClass();Class<?> targetClass = AopUtils.getTargetClass(bean);//判断类上有无controller注解 spring代理类需用spring的注解扫描工具类查找RestController restController = AnnotationUtils.findAnnotation(beanClass, RestController.class);Controller controller = AnnotationUtils.findAnnotation(beanClass, Controller.class);//没有注解直接跳过扫描if (null == controller && null == restController) {return bean;}String applicationName = this.getApplicationName();//项目访问基地址String basePath = this.getBasePath();//如果类上有controller注解再查找requestMapping注解RequestMapping requestMapping = AnnotationUtils.findAnnotation(beanClass, RequestMapping.class);String classMappingPath = this.getClassMappingPath(requestMapping);//按照controller分组上报if (StringUtils.isNotBlank(classMappingPath)) {String controllerGroupPath = basePath + classMappingPath + PASS;ApiSiriusDefinition controllerGroup = new ApiSiriusDefinition();controllerGroup.setGatewayId(gatewayServiceProperties.getGatewayId());controllerGroup.setResource("服务:" + applicationName + ",控制器:" + targetClass.getSimpleName() + ",路径:" + controllerGroupPath);controllerGroup.setUrlPath(controllerGroupPath);apiSiriusDefinitionList.add(controllerGroup);}//查找类中所有方法,进行遍历Method[] methods = targetClass.getMethods();for (Method method : methods) {//查找方法上RequestMapping注解String methodPath = "";String requestType = "";RequestMapping methodRequestMapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);if (methodRequestMapping != null) {String[] value = methodRequestMapping.value();RequestMethod[] requestMethods = methodRequestMapping.method();if (value.length == 0) {if (requestMethods.length == 0) {return bean;}RequestMethod requestMethod = requestMethods[0];requestType = requestMethod.name();if (requestMethod.equals(RequestMethod.POST)) {PostMapping postMapping = AnnotationUtils.findAnnotation(method, PostMapping.class);methodPath = this.joinMethodPath(postMapping.value());} else if (requestMethod.equals(RequestMethod.GET)) {GetMapping getMapping = AnnotationUtils.findAnnotation(method, GetMapping.class);methodPath = this.joinMethodPath(getMapping.value());} else if (requestMethod.equals(RequestMethod.DELETE)) {DeleteMapping deleteMapping = AnnotationUtils.findAnnotation(method, DeleteMapping.class);methodPath = this.joinMethodPath(deleteMapping.value());} else if (requestMethod.equals(RequestMethod.PATCH)) {PatchMapping patchMapping = AnnotationUtils.findAnnotation(method, PatchMapping.class);methodPath = this.joinMethodPath(patchMapping.value());} else if (requestMethod.equals(RequestMethod.PUT)) {PutMapping putMapping = AnnotationUtils.findAnnotation(method, PutMapping.class);methodPath = this.joinMethodPath(putMapping.value());}}ApiSiriusDefinition apiSiriusDefinition = new ApiSiriusDefinition();String urlPath = basePath + classMappingPath + methodPath;apiSiriusDefinition.setUrlPath(urlPath);apiSiriusDefinition.setRequestType(requestType);apiSiriusDefinition.setGatewayId(gatewayServiceProperties.getGatewayId());apiSiriusDefinition.setResource("服务:" + applicationName + ",请求类型:" + requestType + ",路径:" + urlPath);apiSiriusDefinitionList.add(apiSiriusDefinition);}}return bean;}private String joinMethodPath(String[] value) {if (value.length != 0) {String str = this.trimStrWith(value[0], JTR);return JTR + str;}return "";}private String getContextPath() {String contextPath = environment.getProperty(CONTEXT_PATH);contextPath = this.trimStrWith(contextPath, JTR);return StringUtils.isBlank(contextPath) ? "" : contextPath;}public String getApplicationName() {String applicationName = environment.getProperty(APPLICATION_NAME);applicationName = this.trimStrWith(applicationName, JTR);return StringUtils.isBlank(applicationName) ? "" : applicationName;}private String getBasePath() {String contextPath = this.getContextPath();String applicationName = this.getApplicationName();if (StringUtils.isBlank(contextPath)) {return JTR + applicationName;}return JTR + applicationName + JTR + contextPath;}private String getClassMappingPath(RequestMapping requestMapping) {if (null != requestMapping) {String requestMappingUrl = requestMapping.value().length == 0 ? "" : requestMapping.value()[0];requestMappingUrl = this.trimStrWith(requestMappingUrl, JTR);return JTR + requestMappingUrl;}return "";}public String trimStrWith(String str, char trimStr) {if (StringUtils.isBlank(str)) {return str;}int st = 0;int len = str.length();char[] val = str.toCharArray();while ((st < len) && (val[st] <= trimStr)) {st++;}while ((st < len) && (val[len - 1] <= trimStr)) {len--;}return ((st > 0) || (len < str.length())) ? str.substring(st, len) : str;}@Overridepublic void run(String... args) {if (StringUtils.isBlank(this.getApplicationName())) {throw new RuntimeException(APPLICATION_NAME + " should not be null");}if (!apiSiriusDefinitionList.isEmpty()) {log.info("<<<<< start to report api information to api governance platform >>>>>");try {zookeeperService.create(API_DEFINITION + SPLIT + getApplicationName(), JSONArray.toJSONString(apiSiriusDefinitionList));zookeeperService.update(API_DEFINITION + SPLIT + getApplicationName(), JSONArray.toJSONString(apiSiriusDefinitionList));} catch (Exception e) {log.error("reported api information failed,stack info:", e);}log.info("<<<<< successfully reported api information >>>>>");}}}

通过扫描项目下的controller和相应的mapping注解中的属性拼接出url来,通过zk来更新节点数据

2、网关添加监听事件

zk操作查看另一篇文章zookeeper相关操作

/*** @classDesc: 网关核心应用* @author: cyjer* @date: 2023/1/30 9:53*/
@Component
@Slf4j
@RequiredArgsConstructor
public class GatewayApplication implements ApplicationListener<ContextRefreshedEvent> {private final GatewayServiceProperties properties;private final ApiDefinitionService apiDefinitionService;private final ZookeeperService zookeeperService;private final ApiGroupProcesser apiGroupProcesser;@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {//拉取api governance platform 信息apiDefinitionService.refreshApiGovernanceInfo(properties.getGatewayId());log.info("<<<<<<<<<<刷新api分组信息完成>>>>>>>>>>");zookeeperService.create(Constraint.API_DEFINITION, "init");zookeeperService.addWatchChildListener(Constraint.API_DEFINITION, apiGroupProcesser);log.info("<<<<<<<<<<api上报监听器配置完成>>>>>>>>>>");}
}

通过事件处理,首先启动时刷新api信息,同时尝试初始化zk节点,然后注册监听watch。

3、网关监听事件处理

/*** @classDesc: api分组上报* @author: cyjer* @date: 2023/2/10 11:13*/
@Slf4j
@Component
public class ApiGroupProcesser extends AbstractChildListenerProcess implements ApiDefinitionConstraint {@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Resourceprivate GatewayServiceProperties gatewayServiceProperties;@Overridepublic void process(CuratorFramework curatorFramework, PathChildrenCacheEvent cacheEvent) {ChildData data = cacheEvent.getData();if (Objects.nonNull(data) && cacheEvent.getType().equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {log.info("<<<<<<<<<<上报api分组到sentinel>>>>>>>>>>");String path = data.getPath();String content = new String(data.getData(), StandardCharsets.UTF_8);Set<ApiDefinition> definitions = GatewayApiDefinitionManager.getApiDefinitions();List<ApiSiriusDefinition> list = JSONArray.parseArray(content, ApiSiriusDefinition.class);for (ApiSiriusDefinition apiGroup : list) {ApiDefinition api = new ApiDefinition(apiGroup.getResource()).setPredicateItems(new HashSet<ApiPredicateItem>() {{add(new ApiPathPredicateItem().setPattern(apiGroup.getUrlPath()).setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));}});definitions.add(api);}GatewayApiDefinitionManager.loadApiDefinitions(definitions);redisTemplate.opsForHash().put(API_INFO_REDIS_PREFIX + gatewayServiceProperties.getGatewayId(), path, JSONArray.toJSONString(list));log.info("<<<<<<<<<<上报api分组到sentinel成功>>>>>>>>>>");}}
}

5、sentinel控制台启动

java -Dserver.port=8600 -Dcsp.sentinel.dashboard.server=localhost:8600 -Dproject.name=sentinel-dashboard -Xms512m -Xmx512m -Xmn256m -XX:MaxMetaspaceSize=100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/oom/log -Dfile.encoding=UTF-8 -XX:+UseG1GC -jar sentinel-dashboard-1.8.6.jar

打开sentinel控制台,请求几次接口后
在这里插入图片描述

可以看到相应的api分组信息和url路径匹配都已加载,在进行流量治理的时候就可以支持restful接口和controller粒度的治理了

相关文章:

spring cloud gateway集成sentinel并扩展支持restful api进行url粒度的流量治理

sentinel集成网关支持restful接口进行url粒度的流量治理前言使用网关进行总体流量治理&#xff08;sentinel版本&#xff1a;1.8.6&#xff09;1、cloud gateway添加依赖:2、sentinel配置3、网关类型项目配置4、通过zk事件监听刷新上报api分组信息1、非网关项目上报api分组信息…...

wafw00f工具

wafw00f Web应用程序防火墙指纹识别工具 github地址&#xff1a;https://github.com/EnableSecurity/wafw00f 安装环境&#xff1a;python3环境 —>使用 pip install wafw00f 进行安装 安装成功后目录&#xff1a;python安装目录中的Lib\site-packages\wafw00f 本机为&a…...

论文阅读笔记-DiffusionInst: Diffusion Model for Instance Segmentation

文章目录DiffusionInst: Diffusion Model for Instance Segmentation摘要介绍任务介绍实例分割的几种方法想法来源贡献方法整体结构Mask RepresentationDiffusionInst组成TrainingInference不足之处感悟DiffusionInst: Diffusion Model for Instance Segmentation 代码&#x…...

解决CondaUpgradeError网上的方法都不奏效(回退版本、upgrade/update都不行)的问题和CondaValueError

问题描述 Executing transaction: failed ERROR conda.core.link:_execute(502): An error occurred while installing package ‘conda-forge::certifi-2022.9.24-pyhd8ed1ab_0’. CondaUpgradeError: This environment has previously been operated on by a conda version…...

基于某业务单登陆场景并发测试实战

文章目录1 测试目的2 测试目标和测试对象3 名词解释4 测试说明5 测试环境和工具5.1 测试工具5.2 测试环境5.3 人力计划6 测试用例6.1 方案设计6.2 接口地址6.3 接口参数6.3.1 header参数6.3.2 请求参数7 脚本设计8 监控数据8.1 虚拟用户并发情况8.2 事务响应时间8.3 每秒点击次…...

JVM内存模型

程序计数器 多线程时&#xff0c;当线程数超过CPU数量或CPU内核数量&#xff0c;线程之间就要根据时间片轮询抢夺CPU时间资源。因此每个线程有要有一个独立的程序计数器&#xff0c;记录下一条要运行的指令。线程私有的内存区域。如果执行的是JAVA方法&#xff0c;计数器记录正…...

三、NetworkX工具包实战3——特征工程【CS224W】(Datawhale组队学习)

开源内容&#xff1a;https://github.com/TommyZihao/zihao_course/tree/main/CS224W 子豪兄B 站视频&#xff1a;https://space.bilibili.com/1900783/channel/collectiondetail?sid915098 斯坦福官方课程主页&#xff1a;https://web.stanford.edu/class/cs224w NetworkX…...

分布式之Raft共识算法分析

写在前面 在分布式之Paxos共识算法分析 一文中我们分析了paxos算法&#xff0c;知道了其包括basic paxos和multi paxos&#xff0c;并了解了multi paxos只是一种分布式共识算法的思想&#xff0c;而非具体算法&#xff0c;但可根据其设计具体的算法&#xff0c;本文就一起来看…...

数据库——范式

目录 一、概念 二、各范式 第一范式 第二范式 第三范式 BC范式 第四范式 第五范式&#xff08;略&#xff09; 一、概念 基本概念 关系&#xff1a;通常一个关系对应一张表&#xff1b;元组&#xff1a;一行&#xff1b;属性&#xff1a;一列&#xff1b;码&#xff1…...

Geospatial Data Science(2):Geospatial Data in Python

Geospatial Data Science(2):Geospatial Data in Python PART 1: 检查数据 1.1 Imports import geopandas as gpd # for geospatial data handling import osmnx # for handling data from OpenStreetMap (osm) with the help of networkX (nx) import contextily as cx # f…...

16.hadoop系列之MapReduce之MapTask与ReduceTask及Shuffle工作机制

1.MapTask工作机制 以上内容我们之前文章或多或少介绍过&#xff0c;就已网络上比较流行的该图进行理解学习吧 MapTask分为五大阶段 Read阶段Map阶段Collect阶段溢写阶段Merge阶段 2.ReduceTask工作机制 ReduceTask分为三大阶段 Copy阶段Sort阶段Reduce阶段 3.ReduceTask并…...

java 面试过程中遇到的几个问题记录20230220

微服务注册中心的作用微服务注册中心的作用是协调和管理微服务实例的注册和发现。它充当了服务注册表&#xff0c;可以维护服务实例的元数据&#xff0c;例如服务名称、IP 地址和端口号等。当一个微服务启动时&#xff0c;它会向注册中心注册自己的元数据&#xff0c;以使其他服…...

面试题:【数据库三】索引简述

目录 一、索引是什么 二、索引规则 三、索引失效场景 一、索引是什么 索引是帮助Mysql高效获取数据的【数据结构】索引存储在文件系统中索引的文件存储形式与存储引擎相关 mysql有三种存储引擎 InnoDBMyISAMMEMORY索引文件的结构 Hash Hash索引底层是哈希表&#xff0c;哈希…...

数据库必知必会:TiDB(12)TiDB连接管理

数据库必知必会&#xff1a;TiDB&#xff08;12&#xff09;TiDB连接管理TiDB连接管理TiDB的连接特性连接TiDBMySQL命令行客户端图形界面客户端连接其他连接方式写在后面TiDB连接管理 TiDB的连接特性 TiDB Server主要负责接收用户的会话请求&#xff0c;接收SQL并负责SQL语句…...

电源大事,阻抗二字

作者&#xff1a;一博科技高速先生成员 姜杰PCB设计时&#xff0c;我们通常会控制走线的特征阻抗&#xff1b;电源设计时&#xff0c;又会关注电源分配系统&#xff08;PDN&#xff09;的交流阻抗&#xff0c;虽然都是阻抗&#xff0c;一个是信号的通道要求&#xff0c;一个是电…...

ASE20N60-ASEMI的MOS管ASE20N60

编辑-Z ASE20N60在TO-247封装里的静态漏极源导通电阻&#xff08;RDS(ON)&#xff09;为0.4Ω&#xff0c;是一款N沟道高压MOS管。ASE20N60的最大脉冲正向电流ISM为80A&#xff0c;零栅极电压漏极电流(IDSS)为10uA&#xff0c;其工作时耐温度范围为-55~150摄氏度。ASE20N60功耗…...

nginx 代理01(持续更新)

1、如果请求是post&#xff0c;而且请求原是188.188.3.171&#xff0c;处理方式403 if ($request_method ~* "POST") # $request_method 等同于request的method&#xff0c;通常是“GET”或“POST” # 如果访问request的method值为POST则返回“o” {set…...

初阶C语言——操作符【详解】

文章目录1.算术操作符2.移位操作符2.1 左移操作符2.2 右移操作符3.位操作符按位与按位或按位异或4.赋值操作符复合赋值符5.单目操作符5.1单目操作符介绍6.关系操作符7.逻辑操作符8.条件操作符9.逗号表达式10.下标引用、函数调用和结构成员11表达式求值11.1 隐式类型转换11.2算术…...

37k*16 薪,年后直接上岗,3年自动化测试历经3轮面试成功拿下阿里Offer....

前言 转眼过去&#xff0c;距离读书的时候已经这么久了吗&#xff1f;&#xff0c;从18年5月本科毕业入职了一家小公司&#xff0c;到现在快4年了&#xff0c;前段时间社招想着找一个新的工作&#xff0c;前前后后花了一个多月的时间复习以及面试&#xff0c;前几天拿到了阿里…...

利用Rust与Flutter开发一款小工具

1.起因 起因是年前看到了一篇Rust iOS & Android&#xff5c;未入门也能用来造轮子&#xff1f;的文章&#xff0c;作者使用Rust做了个实时查看埋点的工具。其中作者的一段话给了我启发&#xff1a; 无论是 LookinServer 、 Flipper 等 Debug 利器&#xff0c;还是 Flutt…...

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

谷歌浏览器插件

项目中有时候会用到插件 sync-cookie-extension1.0.0&#xff1a;开发环境同步测试 cookie 至 localhost&#xff0c;便于本地请求服务携带 cookie 参考地址&#xff1a;https://juejin.cn/post/7139354571712757767 里面有源码下载下来&#xff0c;加在到扩展即可使用FeHelp…...

如何为服务器生成TLS证书

TLS&#xff08;Transport Layer Security&#xff09;证书是确保网络通信安全的重要手段&#xff0c;它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书&#xff0c;可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...

VTK如何让部分单位不可见

最近遇到一个需求&#xff0c;需要让一个vtkDataSet中的部分单元不可见&#xff0c;查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行&#xff0c;是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示&#xff0c;主要是最后一个参数&#xff0c;透明度…...

Module Federation 和 Native Federation 的比较

前言 Module Federation 是 Webpack 5 引入的微前端架构方案&#xff0c;允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...

Android第十三次面试总结(四大 组件基础)

Activity生命周期和四大启动模式详解 一、Activity 生命周期 Activity 的生命周期由一系列回调方法组成&#xff0c;用于管理其创建、可见性、焦点和销毁过程。以下是核心方法及其调用时机&#xff1a; ​onCreate()​​ ​调用时机​&#xff1a;Activity 首次创建时调用。​…...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题

分区配置 (ptab.json) img 属性介绍&#xff1a; img 属性指定分区存放的 image 名称&#xff0c;指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件&#xff0c;则以 proj_name:binary_name 格式指定文件名&#xff0c; proj_name 为工程 名&…...

JavaScript 数据类型详解

JavaScript 数据类型详解 JavaScript 数据类型分为 原始类型&#xff08;Primitive&#xff09; 和 对象类型&#xff08;Object&#xff09; 两大类&#xff0c;共 8 种&#xff08;ES11&#xff09;&#xff1a; 一、原始类型&#xff08;7种&#xff09; 1. undefined 定…...

GO协程(Goroutine)问题总结

在使用Go语言来编写代码时&#xff0c;遇到的一些问题总结一下 [参考文档]&#xff1a;https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现&#xff1a; 今天在看到这个教程的时候&#xff0c;在自己的电…...

安卓基础(Java 和 Gradle 版本)

1. 设置项目的 JDK 版本 方法1&#xff1a;通过 Project Structure File → Project Structure... (或按 CtrlAltShiftS) 左侧选择 SDK Location 在 Gradle Settings 部分&#xff0c;设置 Gradle JDK 方法2&#xff1a;通过 Settings File → Settings... (或 CtrlAltS)…...