nacos
概述
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以 “服务” 为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
关键特性
服务发现和服务健康监测
Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生 SDK、OpenAPI、或一个 独立的 Agent TODO 注册 Service 后,服务消费者可以使用 DNS TODO 或 HTTP&API 查找和发现服务。
Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP) 和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。 对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测 2 种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。
动态配置服务
动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。
Nacos 提供了一个简洁易用的 UI 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。
动态 DNS 服务
动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单 DNS 解析服务。动态 DNS 服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。
Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP
服务及其元数据管理
Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。
服务 (Service)
服务是指一个或一组软件功能(例如特定信息的检索或一组操作的执行),其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态,如 Kubernetes Service、gRPC|Dubbo RPC Service 或者 Spring Cloud RESTful Service.
服务注册中心 (Service Registry)
服务注册中心,它是服务,其实例及元数据的数据库。服务实例在启动时注册到服务注册表,并在关闭时注销。服务和路由器的客户端查询服务注册表以查找服务的可用实例。服务注册中心可能会调用服务实例的健康检查 API 来验证它是否能够处理请求。
服务元数据 (Service Metadata)
服务元数据是指包括服务端点 (endpoints)、服务标签、服务版本号、服务实例权重、路由规则、安全策略等描述服务的数据
服务提供方 (Service Provider)
是指提供可复用和可调用服务的应用方
服务消费方 (Service Consumer)
是指会发起对某个服务调用的应用方
配置 (Configuration)
在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。
配置管理 (Configuration Management)
在数据中心中,系统中所有配置的编辑、存储、分发、变更管理、历史版本管理、变更审计等所有与配置相关的活动统称为配置管理。
名字服务 (Naming Service)
提供分布式系统中所有对象 (Object)、实体 (Entity) 的 “名字” 到关联的元数据之间的映射管理服务,例如 ServiceName -> Endpoints Info, Distributed Lock Name -> Lock Owner/Status Info, DNS Domain Name -> IP List, 服务发现和 DNS 就是名字服务的 2 大场景。
配置服务 (Configuration Service)

在服务或者应用运行过程中,提供动态配置或者元数据以及配置管理的服务提供者。
- 服务管理: 实现服务 CRUD,域名 CRUD,服务健康状态检查,服务权重管理等功能
- 配置管理: 实现配置管 CRUD,版本管理,灰度管理,监听管理,推送轨迹,聚合数据等功能
- 元数据管理: 提供元数据 CURD 和打标能力
- 插件机制: 实现三个模块可分可合能力,实现扩展点 SPI 机制
- 事件机制: 实现异步化事件通知,sdk 数据变化异步通知等逻辑
- 日志模块: 管理日志分类,日志级别,日志可移植性(尤其避免冲突),日志格式,异常码 + 帮助文档
- 回调机制: sdk 通知数据,通过统一的模式回调用户处理。接口和数据结构需要具备可扩展性
- 寻址模式: 解决 ip,域名,nameserver、广播等多种寻址模式,需要可扩展
- 推送通道: 解决 server 与存储、server 间、server 与 sdk 间推送性能问题
- 容量管理: 管理每个租户,分组下的容量,防止存储被写爆,影响服务可用性
- 流量管理: 按照租户,分组等多个维度对请求频率,长链接个数,报文大小,请求流控进行控制
- 缓存机制: 容灾目录,本地缓存,server 缓存机制。容灾目录使用需要工具
- 启动模式: 按照单机模式,配置模式,服务模式,dns 模式,或者 all 模式,启动不同的程序 + UI
- 一致性协议: 解决不同数据,不同一致性要求情况下,不同一致性机制
- 存储模块: 解决数据持久化、非持久化存储,解决数据分片问题
- Nameserver: 解决 namespace 到 clusterid 的路由问题,解决用户环境与 nacos 物理环境映射问题
- CMDB: 解决元数据存储,与三方 cmdb 系统对接问题,解决应用,人,资源关系
- Metrics: 暴露标准 metrics 数据,方便与三方监控系统打通
- Trace: 暴露标准 trace,方便与 SLA 系统打通,日志白平化,推送轨迹等能力,并且可以和计量计费系统打通
- 接入管理: 相当于阿里云开通服务,分配身份、容量、权限过程
- 用户管理: 解决用户管理,登录,sso 等问题
- 权限管理: 解决身份识别,访问控制,角色管理等问题
- 审计系统: 扩展接口方便与不同公司审计系统打通
- 通知系统: 核心数据变更,或者操作,方便通过 SMS 系统打通,通知到对应人数据变更
- OpenAPI: 暴露标准 Rest 风格 HTTP 接口,简单易用,方便多语言集成
- Console: 易用控制台,做服务管理、配置管理等操作
- SDK: 多语言 sdk
- Agent: dns-f 类似模式,或者与 mesh 等方案集成
- CLI: 命令行对产品进行轻量化管理,像 git 一样好用
创建项目工程
工程:spring_cloud_alibaba
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.eim</groupId> <artifactId>spring_cloud_alibaba</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <url>http://www.funtl.com</url> <modules> <module>hello-spring-cloud-alibaba-dependencies</module> </modules> <properties> <java.version>1.8</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <licenses> <license> <name>Apache 2.0</name> <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> </license> </licenses> <developers> <developer> <id>liwemin</id> <name>Lusifer Lee</name> </developer> </developers> <!-- 项目统一管理的 --> <dependencyManagement> <dependencies> <dependency> <groupId>com.eim</groupId> <artifactId>hello-spring-cloud-alibaba-dependencies</artifactId> <version>${project.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <profiles> <profile> <id>default</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <spring-javaformat.version>0.0.12</spring-javaformat.version> </properties> <build> <plugins> <plugin> <groupId>io.spring.javaformat</groupId> <artifactId>spring-javaformat-maven-plugin</artifactId> <version>${spring-javaformat.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <includes> <include>**/*Tests.java</include> </includes> <excludes> <exclude>**/Abstract*.java</exclude> </excludes> <systemPropertyVariables> <java.security.egd>file:/dev/./urandom</java.security.egd> <java.awt.headless>true</java.awt.headless> </systemPropertyVariables> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <executions> <execution> <id>enforce-rules</id> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <bannedDependencies> <excludes> <exclude>commons-logging:*:*</exclude> </excludes> <searchTransitive>true</searchTransitive> </bannedDependencies> </rules> <fail>true</fail> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-install-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <configuration> <skip>true</skip> </configuration> <inherited>true</inherited> </plugin> </plugins> </build> </profile> </profiles> <repositories> <repository> <id>spring-milestone</id> <name>Spring Milestone</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-snapshot</id> <name>Spring Snapshot</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestone</id> <name>Spring Milestone</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-snapshot</id> <name>Spring Snapshot</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories></project>创建统一的依赖管理
spring-cloud-alibaba-dependencies 是工程的一个子模块。并没有继承工程
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.eim</groupId> <artifactId>hello-spring-cloud-alibaba-dependencies</artifactId> <version>0.0.1-SNAPSHOT</version> <name>hello-spring-cloud-alibaba-dependencies</name> <packaging>pom</packaging>
<properties> <spring.cloud.version>Greenwich.RELEASE</spring.cloud.version> <spring.cloud.alibaba.version>2.1.0.RELEASE</spring.cloud.alibaba.version> </properties>
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<repositories> <repository> <id>spring-milestone</id> <name>Spring Milestone</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> <repository> <id>spring-snapshot</id> <name>Spring Snapshot</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestone</id> <name>Spring Milestone</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> <pluginRepository> <id>spring-snapshot</id> <name>Spring Snapshot</name> <url>https://repo.spring.io/snapshot</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories>
</project>安装nacos
概述
Nacos 官方提供了 Docker 版本,方便我们快速部署,操作步骤如下
操作步骤
Clone 项目
git clone https://github.com/nacos-group/nacos-docker.gitcd nacos-docker单机模式
docker-compose -f example/standalone-mysql.yaml up -d查看日志
第一个 -f 是表示:指定yaml文件
docker-compose -f example/standalone-mysql.yaml logs -fnacos-docker启动失败**
错误信息:bin/docker-startup.sh: line 95: /home/nacos/logs/start.out: Permission denied
Centos7安全Selinux禁止了一些安全权限
解决方法有这么三个:
- 在docker run中加入 —privileged=true 给容器加上特定权限
- 关闭selinux
- 在selinux添加规则,修改挂载目录de
Nacos 控制台
初始密码账号
- 账号: nacos
- 密码: nacos
概述
服务注册中心: 存储服务实例和服务负载均衡策略的数据库

服务提供者
服务提供方: 是指提供可复用和可调用服务的应用方
POM
创建一个名为 hello-spring-cloud-alibaba-provider 的服务提供者项目,pom.xml 配置如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.funtl</groupId> <artifactId>hello-spring-cloud-alibaba</artifactId> <version>0.0.1-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>hello-spring-cloud-alibaba-provider</artifactId> <packaging>jar</packaging> <url>http://www.funtl.com</url> <inceptionYear>2018-Now</inceptionYear> <licenses> <license> <name>Apache 2.0</name> <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> </license> </licenses> <developers> <developer> <id>liwemin</id> <name>Lusifer Lee</name> </developer> </developers> <dependencies> <!-- Spring Boot Begin --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- Spring Boot End --> <!-- Spring Cloud Begin --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- Spring Cloud End --> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.funtl.spring.cloud.alibaba.provider.ProviderApplication</mainClass> </configuration> </plugin> </plugins> </build></project>application.yml
在 application.yml 中配置 Nacos Server 的地址
spring: application: # 服务名 name: service-provider cloud: nacos: discovery: # 服务注册中心 server-addr: 192.168.141.132:8848server: # 服务端口 port: 8070management: # 端点检查(健康检查) endpoints: web: exposure: include: "*"Application
通过 Spring Cloud 原生注解 @EnableDiscoveryClient 开启服务注册发现功能
package com.funtl.spring.cloud.alibaba.provider;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class ProviderApplication { public static void main(String[] args) { SpringApplication.run(ProviderApplication.class, args); }}Controller
编写一个 RestController 提供 RESTFul API 用于测试提供者
package com.funtl.spring.cloud.alibaba.provider.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class EchoController { @GetMapping(value = "/echo/{string}") public String echo(@PathVariable String string) { return "Hello Nacos Provider " + string; }}
安装tar.gz包nacos
1、 下载
地址:https://github.com/alibaba/nacos/releases
nacos-server-2.0.0.tar.gz
解压tar -zxvf nacos-server-2.0.0.tar.gz2、启动,停止
bin目录下 startup.sh 、shutdown.sh
Linux/Unix/Mac启动命令(standalone代表着单机模式运行,非集群模式):
启动sh startup.sh -m standalone
关闭sh shutdown.sh -m standalone3、Nacos支持三种部署模式
- 单机模式 - 用于测试和单机试用。
- 集群模式 - 用于生产环境,确保高可用。
- 多集群模式 - 用于多数据中心场景。
4、配置mysql
单机模式支持mysql
在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:
- 1.安装数据库,版本要求:5.6.5+
- 2.初始化mysql数据库,数据库初始化文件:confi文件夹下nacos-mysql.sql
- 3.修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
spring.datasource.platform=mysql
db.num=1db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=truedb.user=nacos_devtestdb.password=youdontknow再以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql
Nacos Feign 客户端
什么是 Feign
Feign 是一个声明式的伪 HTTP 客户端,它使得写 HTTP 客户端变得更简单。使用 Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用 Feign 注解和 JAX-RS 注解。Feign 支持可插拔的编码器和解码器。Feign 默认集成了 Ribbon,Nacos 也很好的兼容了 Feign,默认实现了负载均衡的效果
- Feign 采用的是基于接口的注解
- Feign 整合了 Ribbon
POM
在 hello-spring-cloud-alibaba-consumer 项目中增加 org.springframework.cloud:spring-cloud-starter-openfeign 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId></dependency>Application
通过 @EnableFeignClients 注解开启 Feign 功能
package com.funtl.spring.cloud.alibaba.consumer;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication@EnableDiscoveryClient@EnableFeignClientspublic class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); }}Service
创建业务结构,通过 @FeignClient("服务名") 注解来指定调用哪个服务
package com.funtl.spring.cloud.alibaba.consumer.service;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;@FeignClient(value = "service-provider")public interface EchoService { @GetMapping(value = "/echo/{string}") String echo(@PathVariable("string") String string);}Controller
package com.funtl.spring.cloud.alibaba.consumer.controller;import com.funtl.spring.cloud.alibaba.consumer.service.EchoService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class TestEchoController { @Autowired private EchoService echoService; @GetMapping(value = "/feign/echo/{str}") public String echo(@PathVariable String str) { return echoService.echo(str); }}验证是否成功
配置负载均衡
- 修改
service-provider服务的端口号如 8071,并启动多个实例,IDEA 中依次点击 Run -> Edit Configurations 并勾选 Allow parallel run 以允许 IDEA 多实例运行项目 - 再次启动
service-provider实例,IDEA Spring Boot 面板可以看到 - Nacos Server 控制台可以看到
service-provider有 2 个实例
服务: 通过预定义接口网络访问的提供给客户端的软件功能
服务名: 服务提供的标识,通过该标识可以唯一确定其指代的服务
服务分组: 不同的服务可以归类到同一分组
虚拟集群: 同一个服务下的所有服务实例组成一个默认集群,集群可以被进一步按需求划分,划分的单位可以是虚拟集群
实例: 提供一个或多个服务的具有可访问网络地址(IP
- 在
service-provider项目的EchoController中增加测试方法
@Value("${server.port}")private String port;@GetMapping(value = "/lb")public String lb() { return "Hello Nacos Provider i am from port: " + port;}- 在
service-consumer项目的EchoService中增加测试方法
@GetMapping(value = "/lb")String lb();- 在
service-consumer项目的TestEchoController中增加测试方法
@GetMapping(value = "/lb")public String lb() { return echoService.lb();}- 重启服务,通过浏览器访问 http://localhost<8080>8080>/lb ,反复刷新浏览器,浏览器交替显示
Hello Nacos Provider i am from port: 8070Hello Nacos Provider i am from port: 8071附:扩展阅读
常见负载均衡策略
负载主机可以提供很多种负载均衡方法,也就是我们常说的调度方法或算法
轮循
Round Robin: 这种方法会将收到的请求循环分配到服务器集群中的每台机器,即有效服务器。如果使用这种方式,所有的标记进入虚拟服务的服务器应该有相近的资源容量 以及负载形同的应用程序。如果所有的服务器有相同或者相近的性能那么选择这种方式会使服务器负载形同。基于这个前提,轮循调度是一个简单而有效的分配请求 的方式。然而对于服务器不同的情况,选择这种方式就意味着能力比较弱的服务器也会在下一轮循环中接受轮循,即使这个服务器已经不能再处理当前这个请求了。 这可能导致能力较弱的服务器超载。
加权轮循
Weighted Round Robin: 这种算法解决了简单轮循调度算法的缺点:传入的请求按顺序被分配到集群中服务器,但是会考虑提前为每台服务器分配的权重。管理员只是简单的通过服务 器的处理能力来定义各台服务器的权重。例如,能力最强的服务器 A 给的权重是 100,同时能力最低的服务器给的权重是 50。这意味着在服务器 B 接收到第一个 请求之前前,服务器 A 会连续的接受到 2 个请求,以此类推。
最少连接数
Least Connection: 以上两种方法都没有考虑的是系统不能识别在给定的时间里保持了多少连接。因此可能发生,服务器 B 服务器收到的连接比服务器 A 少但是它已经超载,因为 服务器 B 上的用户打开连接持续的时间更长。这就是说连接数即服务器的负载是累加的。这种潜在的问题可以通过 “最少连接数” 算法来避免:传入的请求是根据每 台服务器当前所打开的连接数来分配的。即活跃连接数最少的服务器会自动接收下一个传入的请求。接本上和简单轮询的原则相同:所有拥有虚拟服务的服务器资源 容量应该相近。值得注意的是,在流量率低的配置环境中,各服务器的流量并不是相同的,会优先考虑第一台服务器。这是因为,如果所有的服务器是相同的,那么 第一个服务器优先,直到第一台服务器有连续的活跃流量,否则总是会优先选择第一台服务器。
最少连接数慢启动时间
Least Connection Slow Start Time: 对最少连接数和带权重的最小连接数调度方法来说,当一个服务器刚加入线上环境是,可以为其配置一个时间段,在这段时间内连接数是有限制的而且是缓慢 增加的。这为服务器提供了一个‘过渡时间’以保证这个服务器不会因为刚启动后因为分配的连接数过多而超载。这个值在 L7 配置界面设置。
加权最少连接
Weighted Least Connection: 如果服务器的资源容量各不相同,那么 “加权最少连接” 方法更合适:由管理员根据服务器情况定制的权重所决定的活跃连接数一般提供了一种对服务器非常 平衡的利用,因为他它借鉴了最少连接和权重两者的优势。通常,这是一个非常公平的分配方式,因为它使用了连接数和服务器权重比例;集群中比例最低的服务器 自动接收下一个请求。但是请注意,在低流量情况中使用这种方法时,请参考 “最小连接数” 方法中的注意事项。
基于代理的自适应负载均衡
Agent Based Adaptive Balancing: 除了上述方法之外,负载主机包含一个自适用逻辑用来定时监测服务器状态和该服务器的权重。对于非常强大的 “基于代理的自适应负载均衡” 方法来说,负 载主机以这种方式来定时检测所有服务器负载情况:每台服务器都必须提供一个包含文件,这个文件包含一个 0~99 的数字用来标明改服务器的实际负载情况 (0 = 空前,99 = 超载,101 = 失败,102 = 管理员禁用),而服务器同构 http get 方法来获取这个文件;同时对集群中服务器来说,以二进制文件形式提供自身负载情况也是该服务器工作之一,然而,并没有限制服务器如何计算自身的负载 情况。根据服务器整体负载情况,有两种策略可以选择:在常规的操作中,调度算法通过收集的服务器负载值和分配给该服务器的连接数的比例计算出一个权重比 例。因此,如果一个服务器负载过大,权重会通过系统透明的作重新调整。和加权轮循调度方法一样,不正确的分配可以被记录下来使得可以有效的为不同服务器分 配不同的权重。然而,在流量非常低的环境下,服务器报上来的负载值将不能建立一个有代表性的样本;那么基于这些值来分配负载的话将导致失控以及指令震荡。 因此,在这种情况下更合理的做法是基于静态的权重比来计算负载分配。当所有服务器的负载低于管理员定义的下限时,负载主机就会自动切换为加权轮循方式来分 配请求;如果负载大于管理员定义的下限,那么负载主机又会切换回自适应方式。
固定权重
Fixed Weighted: 最高权重只有在其他服务器的权重值都很低时才使用。然而,如果最高权重的服务器下降,则下一个最高优先级的服务器将为客户端服务。这种方式中每个真实服务器的权重需要基于服务器优先级来配置。
加权响应
Weighted Response: 流量的调度是通过加权轮循方式。加权轮循中所使用的权重是根据服务器有效性检测的响应时间来计算。每个有效性检测都会被计时,用来标记它响应成功花 了多长时间。但是需要注意的是,这种方式假定服务器心跳检测是基于机器的快慢,但是这种假设也许不总是能够成立。所有服务器在虚拟服务上的响应时间的总和 加在一起,通过这个值来计算单个服务物理服务器的权重;这个权重值大约每 15 秒计算一次。
源 IP 哈希
Source IP Hash: 这种方式通过生成请求源 IP 的哈希值,并通过这个哈希值来找到正确的真实服务器。这意味着对于同一主机来说他对应的服务器总是相同。使用这种方式,你不需要保存任何源 IP。但是需要注意,这种方式可能导致服务器负载不平衡。
Nacos 分布式配置中心
概述
在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件
什么是 Nacos Config
Nacos 提供用于存储配置和其他元数据的 key/value 存储,为分布式系统中的外部化配置提供服务器端和客户端支持。使用 Spring Cloud Alibaba Nacos Config,您可以在 Nacos Server 集中管理你 Spring Cloud 应用的外部属性配置。
Spring Cloud Alibaba Nacos Config 是 Spring Cloud Config Server 和 Client 的替代方案,客户端和服务器上的概念与 Spring Environment 和 PropertySource 有着一致的抽象,在特殊的 bootstrap 阶段,配置被加载到 Spring 环境中。当应用程序通过部署管道从开发到测试再到生产时,您可以管理这些环境之间的配置,并确保应用程序具有迁移时需要运行的所有内容。
接入配置中心
POM
我们以 service-consumer 项目为例,修改 pom.xml ,引入 Nacos Config Starter
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>Controller
完成上述两步后,应用会从 Nacos Config 中获取相应的配置,并添加在 Spring Environment 的 PropertySources 中。这里我们使用 @Value 注解来将对应的配置注入到 TestEchoController 的 username字段,并添加 @RefreshScope 打开动态刷新功能
package com.funtl.spring.cloud.alibaba.consumer.controller;import com.funtl.spring.cloud.alibaba.consumer.service.EchoService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.cloud.context.config.annotation.RefreshScope;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@RefreshScope@RestControllerpublic class TestEchoController { @Autowired private EchoService echoService; @Value("${user.name}") private String username; @GetMapping(value = "/config") public String config() { return echoService.echo(username); }}使用控制台发布配置
通过浏览器访问 http://192.168.141.132:8848/nacos ,访问 Nacos Server


注意: Data ID 的默认扩展名为
.properties,希望使用 YAML 配置,此处必须指明是.yaml

spring: application: # 服务名 name: service-consumer cloud: nacos: discovery: # 服务注册中心 server-addr: 192.168.141.132:8848server: # 服务端口 port: 8080management: # 端点检查(健康检查) endpoints: web: exposure: include: "*"user: name: "灶门炭治郎"修改客户端配置
创建名为 bootstrap.properties 的配置文件并删除之前创建的 application.yml 配置文件
spring.application.name=service-consumer-configspring.cloud.nacos.config.server-addr=192.168.141.132:8848spring.cloud.nacos.config.file-extension=yaml通过浏览器访问 http://localhost<8080>8080>/config ,浏览器输出如下
Hello Nacos Provider 灶门炭治郎注意: Spring Boot 配置文件的加载顺序,依次为
bootstrap.properties->bootstrap.yml->application.properties->application.yml,其中 bootstrap.properties 配置为最高优先级
动态刷新配置
在 Nacos Server 控制台修改配置文件,将 user.name 属性修改为 灶门祢豆子,此时观察控制台日志,你会发现我们已经成功刷新了配置
验证是否成功
通过浏览器访问 http://localhost<8080>8080>/config ,浏览器输出如下
Hello Nacos Provider 灶门祢豆子提示: 你可以使用
spring.cloud.nacos.config.refresh.enabled=false来关闭动态刷新
附:扩展阅读
Endpoint 信息查看
Spring Boot 应用支持通过 Endpoint 来暴露相关信息,Nacos Config Starter 也支持这一点。在使用之前需要在 maven 中添加 spring-boot-starter-actuator 依赖,并在配置中允许 Endpoints 的访问。
- nacos-discovery: http://localhost<8080>8080>/actuator/nacos-discovery
{ "subscribe": [ { "jsonFromServer": "", "name": "service-consumer", "groupName": "DEFAULT_GROUP", "clusters": null, "cacheMillis": 1000, "hosts": [], "lastRefTime": 0, "checksum": "", "allIPs": false, "key": "service-consumer", "valid": true, "keyEncoded": "service-consumer" }, { "jsonFromServer": "", "name": "service-provider", "groupName": "DEFAULT_GROUP", "clusters": null, "cacheMillis": 1000, "hosts": [], "lastRefTime": 0, "checksum": "", "allIPs": false, "key": "service-provider", "valid": true, "keyEncoded": "service-provider" } ], "NacosDiscoveryProperties": { "serverAddr": "192.168.141.132:8848", "endpoint": "", "namespace": "", "watchDelay": 30000, "logName": "", "service": "service-consumer", "weight": 1, "clusterName": "DEFAULT", "namingLoadCacheAtStart": "false", "metadata": { "preserved.register.source": "SPRING_CLOUD" }, "registerEnabled": true, "ip": "192.168.141.1", "networkInterface": "", "port": 8080, "secure": false, "accessKey": "", "secretKey": "" }}nacos-config: http://localhost<8080>8080>/actuator/nacos-config
{ "NacosConfigProperties": { "serverAddr": "192.168.141.132:8848", "encode": null, "group": "DEFAULT_GROUP", "prefix": null, "fileExtension": "yaml", "timeout": 3000, "endpoint": null, "namespace": null, "accessKey": null, "secretKey": null, "contextPath": null, "clusterName": null, "name": null, "sharedDataids": null, "refreshableDataids": null, "extConfig": null }, "RefreshHistory": [ { "timestamp": "2019-06-30 18:30:45", "dataId": "service-consumer-config.yaml", "md5": "690fd78b5ae9fac5b545c1a8d7ec4e2b" }, { "timestamp": "2019-06-30 18:22:08", "dataId": "service-consumer-config.yaml", "md5": "40b12ffc9d305ec1cad65d606e8f4708" } ], "Sources": [ { "lastSynced": "2019-06-30 18:22:05", "dataId": "service-consumer-config.yaml" } ]}注意: Sources 表示此客户端从哪些 Nacos Config 配置项中获取了信息,RefreshHistory 表示动态刷新的历史记录,最多保存20条,NacosConfigProperties 则为 Nacos Config Starter 本身的配置
更多配置
| 配置项 | key | 默认值 | 说明 |
|---|---|---|---|
| 服务端地址 | spring.cloud.nacos.config.server-addr | ||
| DataId前缀 | spring.cloud.nacos.config.prefix | spring.application.name | |
| Group | spring.cloud.nacos.config.group | DEFAULT_GROUP | |
| dataID后缀及内容文件格式 | spring.cloud.nacos.config.file-extension | properties | dataId的后缀,同时也是配置内容的文件格式,目前只支持 properties |
| 配置内容的编码方式 | spring.cloud.nacos.config.encode | UTF-8 | 配置的编码 |
| 获取配置的超时时间 | spring.cloud.nacos.config.timeout | 3000 | 单位为 ms |
| 配置的命名空间 | spring.cloud.nacos.config.namespace | 常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源隔离等。 | |
| AccessKey | spring.cloud.nacos.config.access-key | ||
| SecretKey | spring.cloud.nacos.config.secret-key | ||
| 相对路径 | spring.cloud.nacos.config.context-path | 服务端 API 的相对路径 | |
| 接入点 | spring.cloud.nacos.config.endpoint | UTF-8 | 地域的某个服务的入口域名,通过此域名可以动态地拿到服务端地址 |
| 是否开启监听和自动刷新 | spring.cloud.nacos.config.refresh.enabled | true |
更多介绍
Nacos为用户提供包括动态服务发现,配置管理,服务管理等服务基础设施,帮助用户更灵活,更轻松地构建,交付和管理他们的微服务平台,基于 Nacos, 用户可以更快速的构建以“服务”为中心的现代云原生应用。Nacos 可以和 Spring Cloud、Kubernetes/CNCF、Dubbo 等微服务生态无缝融合,为用户提供更卓越的体验
Nacos 多环境配置
概述
我们在做项目开发的时候,生产环境和测试环境的一些配置可能会不一样,有时候一些功能也可能会不一样,所以我们可能会在上线的时候手工修改这些配置信息。Spring 为我们提供了 Spring Boot Profile 这个功能(Maven 也为我们提供了 Maven Profile)。我们只需要在启动的时候添加一个虚拟机参数,激活自己环境所要用的 Profile 就可以了。
操作起来很简单,只需要为不同的环境编写专门的配置文件,如:application-dev.yml、application-prod.yml, 启动项目时只需要增加一个命令参数 --spring.profiles.active= 环境配置即可
java -jar 1.0.0-SNAPSHOT.jar --spring.profiles.active=prodspring-cloud-starter-alibaba-nacos-config 在加载配置的时候,不仅仅加载了以 dataid 为 ${spring.application.name}.${file-extension:properties} 为前缀的基础配置,还加载了 dataid 为 ${spring.application.name}-${profile}.${file-extension:properties} 的基础配置。在日常开发中如果遇到多套环境下的不同配置,可以通过 Spring 提供的 ${spring.profiles.active} 这个配置项来配置。
注意:配置文件的加载顺序(nacos读取配置文件一定要在配置文件中处于第一优先级)
bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
使用 Nacos Config Profile
我们以 service-provider 项目为例,演示多环境配置效果,不要忘记依赖 Nacos Config Starter
1、依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>3、使用控制台发布配置
注意: 控制台发布配置时不要加注释,否则打成 Jar 包后运行会报无法解析配置文件的错误
通过浏览器访问 http://192.168.141.132:8848/nacos ,访问 Nacos Server
- 发布一个名为
service-provider-config-dev.yaml的 测试环境 配置
spring: application: # 服务名 name: service-provider cloud: nacos: discovery: # 服务注册中心 server-addr: 192.168.141.132:8848server: # 服务端口 port: 8070management: # 端点检查(健康检查) endpoints: web: exposure: include: "*"- 发布一个名为
service-provider-config-prod.yaml的 生产环境 配置
spring: application: # 服务名 name: service-provider cloud: nacos: discovery: # 服务注册中心 server-addr: 192.168.141.132:8848server: # 修改了上面的端口号,区分配置的不同 port: 8071management: # 端点检查(健康检查) endpoints: web: exposure: include: "*"4、修改客户端配置
配置注册中心地址,由于配置中心的优先级高,所有必须将注册中心配置在最底层(bootstrap.properties)中
- 创建名为
bootstrap.properties的配置文件并删除之前创建的application.yml配置文件
spring.profiles.active=devspring.application.name=service-provider-configspring.cloud.nacos.config.server-addr=192.168.141.132:8848spring.cloud.nacos.config.file-extension=yaml- 创建名为
bootstrap-prod.properties的配置文件
spring.application.name=service-provider-configspring.cloud.nacos.config.server-addr=192.168.141.132:8848spring.cloud.nacos.config.file-extension=yaml测试多环境配置
此时我们有两个配置文件,分别为 bootstrap.properties 和 bootstrap-prod.properties ,我们需要指定启动时加载哪一个配置文件
不指定:
不指定默认时 dev,则加载:
1、
bootstrap.properties2、 service-provider-config-dev.yaml
指定pro:
java -Xms512m -Xmx512m -Xmn200m -Xss256k -jar demo996.jar --spring.profiles.active=dev则加载:
1、
bootstrap.properties2、bootstrap-prod.properties
bootstrap-prod.properties中的相同内容会覆盖bootstrap.properties中的内容,不相同的不会覆盖
3、
service-provider-config-prod.yaml
运行项目并观察日志
application 和 bootstrap关系
(一)、关系
- Spring Cloud 构建于 Spring Boot 之上,在 Spring Boot 中有两种上下文,一种是 bootstrap,另外一种是 application,
- application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
- bootstrap 是应用程序的父上下文,也就是说 bootstrap 加载优先于 applicaton。
- bootstrap 主要用于从额外的资源来加载配置信息,还可以在本地外部配置文件中解密属性。
- 这两个上下文共用一个环境,它是任何Spring应用程序的外部属性的来源。
- bootstrap 里面的属性会优先加载,它们默认也不能被本地相同配置覆盖。
- boostrap 由父 ApplicationContext 加载,比 applicaton 优先加载
- boostrap 里面的属性不能被覆盖
(二)、bootstrap 配置文件有以下几个应用场景。
- 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
- 一些固定的不能被覆盖的属性
- 一些加密/解密的场景;
注意:一旦bootStrap.yml 被加载,则内容不会被覆盖,即便后期加载的application.yml的内容标签与bootstrap的标签一致,application 也不会覆盖bootstrap, 而application.yml 里面的内容可以动态替换。
nacos配置中心命名空间:
1、新增nacos命名空间

每个命名空间都有一个命名空间ID

在命名空间下新增配置
dataid 文件名
group分组
2、nacos实战配置

spring: application: name: gulimall-gateway cloud: nacos: config: server-addr: 49.232.154.66:8848 namespace: 41812423-7274-4c60-a5d0-15244148f1a4 extension-configs: - dataId: gulimall-gateway.yml group: devnacos注册中心
1、pom.xml
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>2、配置服务注册与发现
spring: application: name: gulimall-gateway cloud: nacos: discovery: server-addr: 49.232.154.66:88483、开启服务注册与发现
@EnableDiscoveryClient
@EnableDiscoveryClient@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)public class GulimallGatewayApplication {
public static void main(String[] args) { SpringApplication.run(GulimallGatewayApplication.class, args); }
}nacos动态刷新
nacos动态刷新+@value实战
一、@value
@Value 读取配置
1、读集合
在日常开发中,经常会遇到需要在配置文件中,存储 List 或是 Map 这种类型的数据。
Spring 原生是支持这种数据类型的,以配置 List 类型为例,对于 .yml 文件配置如下:
test: list: - aaa - bbb - ccc对于 .properties 文件配置如下所示:
test.list[0]=aaatest.list[1]=bbbtest.list[2]=ccc当我们想要在程序中使用时候,想当然的使用 @Value 注解去读取这个值,就像下面这种写法一样:
@Value("${test.list}")private List testList;你会发现程序直接报错了,报错信息如下:
java.lang.IllegalArgumentException: Could not resolve placeholder 'test.list' in value "${test.list}"这个问题也是可以解决的,以我们要配置的 key 为 test.list 为例,新建一个 test 的配置类,将 list 作为该配置类的一个属性:
@Configuration@ConfigurationProperties("test")public class TestListConfig { private List list; public List getList() { return list; } public void setList(List list) { this.list = list; }}在程序其他地方使用时候。采用自动注入的方式,去获取值:
@Autowiredprivate TestListConfig testListConfig;
// testListConfig.getList();可以看见,这种方式十分的不方便,最大的问题是配置和代码高耦合了,增加一个配置,还需要对配置类做增减改动。关注公众号Java技术栈可以搜索阅读Spring Boot更多强大的参数配置和读取教程。
2、数组怎么样
数组?说实话,业务代码写多了,这个“古老”的数据结构远远没有 list 用的多,但是它在解决上面这个问题上,出乎异常的好用。
test: array1: aaa,bbb,ccc array2: 111,222,333 array3: 11.1,22.2,33.3@Value("${test.array1}")private String[] testArray1;
@Value("${test.array2}")private int[] testArray2;
@Value("${test.array3}")private double[] testArray3;这样就能够直接使用了,就是这么的简单方便,如果你想要支持不配置 key 程序也能正常运行的话,给它们加上默认值即可:
@Value("${test.array1:}")private String[] testArray1;
@Value("${test.array2:}")private int[] testArray2;
@Value("${test.array3:}")private double[] testArray3;仅仅多了一个 : 号,冒号后的值表示当 key 不存在时候使用的默认值,使用默认值时数组的 length = 0。
总结下使用数组实现的优缺点:
优点:
- 不需要写配置类
- 使用逗号分割,一行配置,即可完成多个数值的注入,配置文件更加精简
缺点:
- 业务代码中数组使用很少,基本需要将其转换为 List,去做 contains、foreach 等操作。
3、替代方法
那么我们有没有办法,在解析 list、map 这些类型时,像数组一样方便呢?
答案是可以的,这就依赖于 EL 表达式。
3.1 解析 List
以使用 .yml 文件为例,我们只需要在配置文件中,跟配置数组一样去配置:
test: list: aaa,bbb,ccc在调用时,借助 EL 表达式的 split() 函数进行切分即可。
@Value("#{'${test.list}'.split(',')}")private List testList;同样,为它加上默认值,避免不配置这个 key 时候程序报错:
@Value("#{'${test.list:}'.split(',')}")private List testList;但是这样有个问题,当不配置该 key 值,默认值会为空串,它的 length = 1(不同于数组,length = 0),这样解析出来 list 的元素个数就不是空了。
这个问题比较严重,因为它会导致代码中的判空逻辑执行错误。这个问题也是可以解决的,在 split() 之前判断下是否为空即可。
@Value("#{'${test.list:}'.empty ? null : '${test.list:}'.split(',')}")private List testList;如上所示,即为最终的版本,它具有数组方式的全部优点,且更容易在业务代码中去应用。线程安全的List,这篇推荐看下。
3.2 解析 Set
解析 Set 和解析 List 本质上是相同的,唯一的区别是 Set 会做去重操作。
test: set: 111,222,333,111`@Value("#{'${test.set:}'.empty ? null : '${test.set:}'.split(',')}")private Set testSet;// output: [111, 222, 333]3.3 解析 Map
解析 Map 的写法如下所示,value 为该 map 的 JSON 格式,注意这里使用的引号:整个 JSON 串使用引号包裹,value 值使用引号包裹。Spring Boot 返回 JSON 数据,推荐大家看下。
test: map1: '{"name": "zhangsan", "sex": "male"}' map2: '{"math": "90", "english": "85"}'在程序中,利用 EL 表达式注入:
@Value("#{${test.map1}}")private Map map1;@Value("#{${test.map2}}")private Map map2;注意,使用这种方式,必须得在配置文件中配置该 key 及其 value。我在网上找了许多资料,都没找到利用 EL 表达式支持不配置 key/value 的写法。
如果你真的很需要这个功能,就得自己写解析方法了,这里以使用 fastjson 进行解析为例:
(1) 自定义解析方法
public class MapDecoder { public static Map decodeMap(String value) { try { return JSONObject.parseObject(value, new TypeReference>(){}); } catch (Exception e) { return null; } }}(2) 在程序中指定解析方法
@Value("#{T(com.github.jitwxs.demo.MapDecoder).decodeMap('${test.map1:}')}")private Map map1;@Value("#{T(com.github.jitwxs.demo.MapDecoder).decodeMap('${test.map2:}')}")private Map map2;二、动态刷新测试
@RefreshScope注解
@RefreshScope@Slf4j@Service("cmsJwtTokenHandler")public class JwtTokenHandlerCmsImpl{
@Value("#{'${test.list:}'.empty ? null : '${test.list:}'.split(',')}") private List testList;
}测试:
配置中心更改后,路径自动生效
遇到的报错:
nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder ‘XXX’ in value “${XXX}”
出现这种问题的几种原因:
1、你的配置文件properties 或者 yml文件,没有写上对应的XXX及对应的值
2、在引用的地方加上默认值,格式是 @Value(”${xxx
3、多环境配置的时候,默认的文件没有配置上你调用的自定义值
这个情况比较特殊,是因为你的多环境配置文件是这样的时候,记住,是每个环境及默认的配置文件都要加上你的自定义属性。
一般配置在远端激活的配置文件中可避免出错,eg: active
;则配置参数在nacos配置中心里面的 xx-dev.yml 中
如果上述的默认配置文件你没加自定义属性,会导致本地开发的时候报上述错误。 而你打包的时候并不会报错,因为你勾选了环境,只有idea启动的时候会报错,一开始让我还纳闷了好一
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时