• SpringCloud Alibaba 入门到精通 - Nacos


    本文旨在总结Alibaba常用组件及技术关键点,希望可以帮助到路过的朋友。Alibaba的cloud解决方案在github上是热度最高的一个cloud方案,也是时下最为流行的解决方案,所以他的原理和内幕还是值得我们一探的。这里不做微服务架构演进,微服务基础知识等的普及,如果需要可以进这里进行了解:Springcloud netfilx常用组件

    一、基础结构搭建

    这里通过maven的父子工程来管理本次展示的demo。使用父子观察管理的好处是:

    • 1.代码集中方便集中管理,下载
    • 2.集中化管理学习成本更低项目易于接受

    1.父工程创建

    使用IDEA。file–>new–>project–>maven 来进行创建父工程,父工程一定得是pom的,这样我们编译时父工程才可以实现管理子工程的目的,如果不是pom的我们是无法在父工程下正确创建子工程的。
    本次demo采用集中的版本管理,我们只需要限定以下三个版本即可,只需要将他们放在dependencyManagement中进行指明版本,同时声明type和scope就可以做到全局的组件版本控制了,功能类似于parent标签,不过parent只能声明一个父工程来管理版本依赖,而放在dependencyManagement的dependency却可以支持多个。

    • SpringBoot 版本:2.6.11
    • SpringCloud 版本:2021.0.4
    • SpringCould Alibaba 版本:2021.0.4.0

    若是对alibaba与cloud和boot的版本对应关系不清晰,可以看这里:SpringCloud组件版本依赖关系

    <dependencyManagement>
    	<dependencies>
    		<dependency>
    		    <groupId>com.alibaba.cloudgroupId>
    		    <artifactId>spring-cloud-alibaba-dependenciesartifactId>
    		    <version>${Spring-cloud-alibaba-version}version>
    		    <type>pomtype>
    		    <scope>importscope>
    		dependency>
    	<dependencies>	
    dependencyManagement>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    下面是父工程的完整pom文件

    
    <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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
    
        
        
        
        
        
        
        
        <groupId>com.chenggroupId>
        <artifactId>spring-cloud-alibaba-baseartifactId>
        <packaging>pompackaging>
        <version>1.0.0version>
        <modules>
            <module>order-servermodule>
            <module>stock-servermodule>
        modules>
    
        <name>spring-cloud-alibaba-basename>
        <description>alibaba父工程description>
        <properties>
            <java.version>8java.version>
            <Spring-boot-version>2.6.11Spring-boot-version>
            <Spirng-cloud-version>2021.0.4Spirng-cloud-version>
            <Spring-cloud-alibaba-version>2021.0.4.0Spring-cloud-alibaba-version>
        properties>
    
        <dependencyManagement>
            
            <dependencies>
                
                
                
                <dependency>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-starter-parentartifactId>
                    <version>${Spring-boot-version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
    
                
                <dependency>
                    <groupId>com.alibaba.cloudgroupId>
                    <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                    <version>${Spring-cloud-alibaba-version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
    
                
                <dependency>
                    <groupId>org.springframework.cloudgroupId>
                    <artifactId>spring-cloud-dependenciesartifactId>
                    <version>${Spirng-cloud-version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
    
        dependencyManagement>
    
    
        
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starterartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                plugin>
            plugins>
        build>
    
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    2.子工程创建

    子工程创建都是一样的,这里展示一个例子:父工程右键–>new–>module–>spring-initializr,这里不使用spring-initializr,使用maven也是可以的,只是使用spring-initializr会方便一些,无需我们自己创建启动类、配置文件等。若是你的IDEA比较新(据我所知IDEA2021以下好像用不了)还可以使用spring-initializr指定三方的脚手架,三方脚手架地址推荐使用阿里的。https://start.aliyun.com,这个脚手架对Springcloud组件支持的更友好,也包含更多的阿里系的组件
    注意:社区版的IDEA不支持Spring Initializr功能
    在这里插入图片描述
    下面是子工程的pom文件:

    
    <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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
        <parent>
            <groupId>com.chenggroupId>
            <artifactId>spring-cloud-alibaba-baseartifactId>
            <version>1.0.0version>
        parent>
        <groupId>com.chenggroupId>
        <artifactId>order-serverartifactId>
        <version>1.0.0version>
        <name>order-servername>
        <description>Demo project for Spring Bootdescription>
        <properties>
            <java.version>17java.version>
        properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                plugin>
            plugins>
        build>
    
    project>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    到这里就简单搭建了一个架子出来,后面就是为每个服务进行引入不同的组件和依赖了。
    若是感觉以上父子工程创建不够详细,推荐看这里:Maven创建父子工程详解

    二、Nacos:注册中心

    Nacos是使用SpringCloud Alibaba的必用组件了,他统一了Eureka和Config,同时支持优雅的服务下线,对服务进行管理可以很从容的实现服务替换。优点还有很多不一一列举了,反正是肯定比eureka和config好用。上面我们已经指定了Alibaba的版本,这个版本里限定的Nacos版本是:2.0.4 所以这里的服务端、客户端我们都是用2.0.4的Nacos来进行搭建。下图是主流的注册中心:
    在这里插入图片描述
    Nacos默认采用AP模式,Eureka也是AP模式,Zookeeper是CP模式,设计原则不同与他们最初的设计目的紧密相关的。Eureka设计的唯一目的是为了作为注册中心,注册中心不需要强一致性(服务的注册与发现本身都是定时任务就无法做到完全实时,业务上也没有这个必要),但是对于可用性要求比较高,所以Eureka遵循的是AP原则,而Zookeeper设计目的不只是注册中心,他还有发布订阅、集群管理、分布式锁、负载均衡等功能,而这些对一致性要求比较高,所以zookeeper遵循的是CP原则,Nacos虽然支持CP和APd但是一般都是使用的默认的AP原则,因为使用Nacos一般是作为注册、配置中心,这两项都不需要强一致性:注册中心本来也是需要客户端将注册信息拉取到本地才会起作用,而这个拉取本来就是有10ms(nacos)的间隔,配置中心如果不使用@RefreshScope的话其实都只是启动服务去读取了一次配置信息,而即使使用@RefreshScope。ms级别的延迟对业务也不会有任何影响。所以Nacos默认是AP。

    1.服务端搭建

    这里服务端的搭建使用docker来进行安装,这样会比较快和省事。这里采用的思路是:先下载一个基础的nacos镜像,不配置数据卷。下载完毕后将默认的配置文件cp出来进行更改,然后删除原容器。从新启动一个新的镜像。这里需要特别注意的是9848/9849两个端口的暴露,这俩是必须的端口,测试时感觉不到问题,但是部署线上肯定会有问题,加上就完事了。如果不是使用的Docker,那服务器也必须把这俩端口放开。这俩端口是在8848的基础上进行+1000,和+1001来的,如果你用的不是8848那就根据+1000、+1001来重新计算即可。

    • 第一步:下载基础镜像

      docker  run \
      --name my-nacos -d \
      -p 8848:8848 \
      -p 9848:9848 \
      -p 9849:8849 \
      nacos/nacos-server:v2.0.4
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      新建文件夹,然后将镜像内的配置文件cp到我们的文件夹

      mkdir -p /apps/nacos/logs/                      
      mkdir -p /apps/nacos/conf/  
      cd /apps/nacos/conf/ 
      docker cp my-nacos:/home/nacos/conf/application.properties ./
      
      • 1
      • 2
      • 3
      • 4
    • 第二步:修改本地的配置文件
      这个本地配置文件是指刚刚我们从容器内部cp出来的配置文件,更改下面几项即可,这里的数据库放在了第四步
      注意:spring.datasource.platform 这个配置项在后面的版本进行了变更,变为了:spring.sql.init.platform(好像是2.2.3开始变得)

      spring.datasource.platform=mysql
      db.num=1
      db.url.0=jdbc:mysql://192.168.*.*:3306/nacos_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
      db.user.0=root
      db.password.0=root
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 第三步: 重启nacos
      注意这里是以单机模式来运行的,重启完成后可以去看看日志是否启动正常了,如果正常直接访问即可:ip:8848/nacos
      注意这里是无法直接访问的,登录会提示用户名和密码错误(nacos/nacos),f12会看到具体错误,请看后面的“登录报错”来进行解决。此外还需要注意:服务是否启动成功不能只看docker logs的结果,还需要看nacos的日志nacos.log是否成功,需要两个都完成了,才可以正常加载nacos的登录页面,如果碰到docker logs显示启动成功,但是还是无法加载nacos登录页,记得看下nacos.log日志是否也显示完成,nacos.log这个会比较慢,集群模式下有时候需要2-3分钟

      docker  run \
      --name my-nacos -d \
      -p 8848:8848 \
      -p 9848:9848 \
      -p 9849:8849 \
      --env PREFER_HOST_MODE=ip --env MODE=standalone \
      -v /apps/nacos/logs:/home/nacos/logs \
      -v /apps/nacos/conf/application.properties:/home/nacos/conf/application.properties \
      nacos/nacos-server:v2.0.4
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 第四步:数据库创建
      下面是数据库、表,不过还是推荐去官网下载包自己找,防止sql有变化:
      官网下载地址:https://github.com/alibaba/nacos/releases?page=3
      数据库脚本相对地址:./nacos-server-1.4.1/nacos/conf/nacos-mysql.sql

      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = config_info   */
      /******************************************/
      CREATE TABLE `config_info` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
        `data_id` varchar(255) NOT NULL COMMENT 'data_id',
        `group_id` varchar(255) DEFAULT NULL,
        `content` longtext NOT NULL COMMENT 'content',
        `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
        `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
        `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
        `src_user` text COMMENT 'source user',
        `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
        `app_name` varchar(128) DEFAULT NULL,
        `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
        `c_desc` varchar(256) DEFAULT NULL,
        `c_use` varchar(64) DEFAULT NULL,
        `effect` varchar(64) DEFAULT NULL,
        `type` varchar(64) DEFAULT NULL,
        `c_schema` text,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
      
      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = config_info_aggr   */
      /******************************************/
      CREATE TABLE `config_info_aggr` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
        `data_id` varchar(255) NOT NULL COMMENT 'data_id',
        `group_id` varchar(255) NOT NULL COMMENT 'group_id',
        `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
        `content` longtext NOT NULL COMMENT '内容',
        `gmt_modified` datetime NOT NULL COMMENT '修改时间',
        `app_name` varchar(128) DEFAULT NULL,
        `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
      
      
      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = config_info_beta   */
      /******************************************/
      CREATE TABLE `config_info_beta` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
        `data_id` varchar(255) NOT NULL COMMENT 'data_id',
        `group_id` varchar(128) NOT NULL COMMENT 'group_id',
        `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
        `content` longtext NOT NULL COMMENT 'content',
        `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
        `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
        `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
        `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
        `src_user` text COMMENT 'source user',
        `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
        `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
      
      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = config_info_tag   */
      /******************************************/
      CREATE TABLE `config_info_tag` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
        `data_id` varchar(255) NOT NULL COMMENT 'data_id',
        `group_id` varchar(128) NOT NULL COMMENT 'group_id',
        `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
        `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
        `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
        `content` longtext NOT NULL COMMENT 'content',
        `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
        `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
        `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
        `src_user` text COMMENT 'source user',
        `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
      
      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = config_tags_relation   */
      /******************************************/
      CREATE TABLE `config_tags_relation` (
        `id` bigint(20) NOT NULL COMMENT 'id',
        `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
        `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
        `data_id` varchar(255) NOT NULL COMMENT 'data_id',
        `group_id` varchar(128) NOT NULL COMMENT 'group_id',
        `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
        `nid` bigint(20) NOT NULL AUTO_INCREMENT,
        PRIMARY KEY (`nid`),
        UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
        KEY `idx_tenant_id` (`tenant_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
      
      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = group_capacity   */
      /******************************************/
      CREATE TABLE `group_capacity` (
        `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
        `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
        `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
        `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
        `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
        `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
        `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
        `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
        `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
        `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_group_id` (`group_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
      
      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = his_config_info   */
      /******************************************/
      CREATE TABLE `his_config_info` (
        `id` bigint(64) unsigned NOT NULL,
        `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        `data_id` varchar(255) NOT NULL,
        `group_id` varchar(128) NOT NULL,
        `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
        `content` longtext NOT NULL,
        `md5` varchar(32) DEFAULT NULL,
        `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `src_user` text,
        `src_ip` varchar(50) DEFAULT NULL,
        `op_type` char(10) DEFAULT NULL,
        `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
        PRIMARY KEY (`nid`),
        KEY `idx_gmt_create` (`gmt_create`),
        KEY `idx_gmt_modified` (`gmt_modified`),
        KEY `idx_did` (`data_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
      
      
      /******************************************/
      /*   数据库全名 = nacos_config   */
      /*   表名称 = tenant_capacity   */
      /******************************************/
      CREATE TABLE `tenant_capacity` (
        `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
        `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
        `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
        `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
        `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
        `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
        `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
        `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
        `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
        `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_tenant_id` (`tenant_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
      
      
      CREATE TABLE `tenant_info` (
        `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
        `kp` varchar(128) NOT NULL COMMENT 'kp',
        `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
        `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
        `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
        `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
        `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
        `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
        PRIMARY KEY (`id`),
        UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
        KEY `idx_tenant_id` (`tenant_id`)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
      
      CREATE TABLE `users` (
      	`username` varchar(50) NOT NULL PRIMARY KEY,
      	`password` varchar(500) NOT NULL,
      	`enabled` boolean NOT NULL
      );
      
      CREATE TABLE `roles` (
      	`username` varchar(50) NOT NULL,
      	`role` varchar(50) NOT NULL,
      	UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
      );
      
      CREATE TABLE `permissions` (
          `role` varchar(50) NOT NULL,
          `resource` varchar(255) NOT NULL,
          `action` varchar(8) NOT NULL,
          UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
      );
      
      INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
      
      INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166
      • 167
      • 168
      • 169
      • 170
      • 171
      • 172
      • 173
      • 174
      • 175
      • 176
      • 177
      • 178
      • 179
      • 180
      • 181
      • 182
      • 183
      • 184
      • 185
      • 186
      • 187
      • 188
      • 189
      • 190
      • 191
      • 192
      • 193
      • 194
      • 195
      • 196
      • 197
      • 198
      • 199
      • 200
      • 201
      • 202
    • 第五步:集群配置
      nacos的集群模式很简单,只需要两步操作即可:

      • 1.保证多个结点的数据库配置一致,配置文件一致,nacos基于数据库实现
        这个点无需多说,保证一致即可
      • 2.更改配置信息,这里使用的是docker,可以直接在启动命令上进行变更
        增加如下配置:
        // 声明集群模式
        --env MODE=cluster
        // 这个解决ip展示问题,和集群无关
        --network=host
        // 声明其他节点信息,无需写自己的,多节点逗号隔开
        --env NACOS_SERVERS=10.3.8.135:8848,10.3.8.136:8848
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        更改后的完整命令如下:
        // 节点1启动命令
        docker  run \
        -p 8848:8848 \
        -p 9848:9848 \
        -p 9849:8849 \
        --name my-nacos -d \
        --network=host \
        --env PREFER_HOST_MODE=ip --env MODE=cluster \
        --env NACOS_SERVERS=192.168.150.185:8848 \
        -v /apps/nacos/logs:/home/nacos/logs \
        -v /apps/nacos/conf/application.properties:/home/nacos/conf/application.properties \
        nacos/nacos-server:v2.0.4
        
        
        
        // 节点2命令
        docker  run \
        -p 8848:8848 \
        -p 9848:9848 \
        -p 9849:8849 \
        --name my-nacos -d \
        --network=host \
        --env PREFER_HOST_MODE=ip --env MODE=cluster \
        --env NACOS_SERVERS=192.168.150.148:8848 \
        -v /apps/nacos/logs:/home/nacos/logs \
        -v /apps/nacos/conf/application.properties:/home/nacos/conf/application.properties \
        nacos/nacos-server:v2.0.4
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
        • 27
        搭建完成后,进入nacos可以在集群管理中看到我们的集群:
        在这里插入图片描述
    • 特别注意:登录报错
      注意nacos现在安装完毕以后是肯定登录不了的,登录时f12 会提示这个错误:caused: The specified key byte array is 0 bits which is not secure enough for any JWT HMAC-SHA algorithm.The JWT JWA Specification(RFC 7518, Section 3.2)states that keys used with HMAC-SHA algorithms MUST have a size>=256 bits(the key size must be greater
      这需要我们为配置文件增加一个默认配置,现在nacos官方删除了这个默认配置(有安全隐患,万一把地址暴露到公网别人用这个秘钥访问你就会有隐患),所以需要我们自己添加到application.properties中,不添加是登录不了的,也无法使用。

      # 秘钥可自行更改,我试了2.0.4和1.4.6版本都是无法直接登录的,demo测试日期:2023.09 ,后面配置参数可能会变化,如果不行建议官网查下参数
      nacos.core.auth.default.token.secret.key=SecretKey012345678901234567890123456789012345678901234567890123456789
      
      • 1
      • 2

      nacos官方文档:nacos官方文档
      这是官方文档位置(我下载的是1.4.6和2.0.4也是没有这个参数的,这个描述有点坑爹):在这里插入图片描述

    2.注册中心-客户端搭建

    • 第一步:添加依赖
      增加依赖项,因为父工程已经声明了依赖版本,所以这里直接添加依赖即可

      
      
          com.alibaba.cloud
          spring-cloud-starter-alibaba-nacos-discovery
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 第二步:修改配置文件
      当前版本已无需在启动类增加注解:@EnableDiscoveryClient了,当然加了也不会报错。我们只需要配置好配置文件就ok了。

      # 注册服务时会携带端口
      server:
        port: 8001
      
      # nacos中的服务名默认读取的是spring.application.name
      # 如果需要自定义,则需要通过spring.cloud.nacos.discovery.service-name来指定
      spring:
        application:
          name: order-server
        cloud:
          nacos:
            # 这里配置集群时,启动时默认只会连接其中一个,但是若是有节点挂掉,可以自动切换到另外的结点
            server-addr: 192.168.150.185:8848,192.168.150.148:8848
            user-name: nacos # 这是默认值
            password: nacos # 这是默认值
            namespace: public # 这是默认值
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

      以上是基础配置,配置完成后启动服务即可成功注册:
      在这里插入图片描述

    • 特别注意:

      • 1.可以不添加启动类注解:@EnableDiscoveryClient
      • 2.当前版本的nacos已默认不集成Ribbon(之前版本默认集成Ribbon)
      • 3.配置集群时,虽然声明多个结点,但启动项目只会往其中一个去注册,只有注册的结点挂了才会自动切换到另一个结点
      • 4.真实场景中若是配置nginx的集群,一般不会直接在配置文件中声明集群,而是通过ngxin来做负载均衡实现集群,nginx的负载这里就不介绍了。

    3.注册中心-管理页面

    • 1.命名空间和分组的概念
      其实都是为了服务分组来设置出的概念,主要是看怎么用,nacos设计之初估计想的是命名空间作为环境区分,分组用来作为服务区分,但是在使用时,因为不同环境都是搭建了不同的nacos服务,所以一般我们不使用命名空间作为环境区分,基本都是使用分组的概念来对服务进行区分。
    • 2.服务详情-保护阈值
      这个的设定主要是为了做“雪崩保护”使用的。这个值只能设定在0-1之间。假如我们设定的是0.5,假如我们有三个节点。那么若是当存活服务/总服务,的值小于0.5时,就会触发雪崩保护,雪崩保护会启用所有节点,即使该节点不在线。雪崩保护主要是为了防止所有请求都打到一个结点上导致整个集群全部宕机的风险。前面说过nacos服务端30s收不到心跳会下线nacos客户端,这个下线只会下线临时节点,我们注册的结点信息都是临时节点,若是想要服务一直存在我们可以注册非临时节点,雪崩保护只有我们注册的是非临时节点才有可能触发。下面是非临时节点的注册方式:
      spring:
        application:
          name: order-server
        cloud:
          nacos:
            # 这里配置集群时,启动时默认只会连接其中一个,但是若是有节点挂掉,可以自动切换到另外的结点
            server-addr: 192.168.150.185:8848,192.168.150.148:8848
            discovery:
              ephemeral: false # 是否是临时实例,若是false,则即使服务端超过30s没有收到心跳,也不会剔除,而是永久存在
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 3.服务详情-权重
      这个就是负载的权重了,权重值越高调用的次数就会越多,粗略算法可以采用当前的权重除以所有节点的权重之和。
    • 4.服务下线
      这里的服务下线大概20s之后会起作用(nacos客户端会定时拉取服务端的列表),下线后的服务虽然是健康的,但是不会被nacos客户端拉取到本地。

    4.注册中心-常用配置

    下面是和nacos相关的一些常用配置了。大部分其实使用默认值即可无需更改

    # nacos中的服务名默认读取的是spring.application.name
    # 如果需要自定义,则需要通过spring.cloud.nacos.discovery.service来指定
    spring:
      application:
        name: order-server
      cloud:
        nacos:
          # 这里配置集群时,启动时默认只会连接其中一个,但是若是有节点挂掉,可以自动切换到另外的结点
          server-addr: 192.168.150.185:8848,192.168.150.148:8848
          user-name: nacos # 这是默认值
          password: nacos # 这是默认值
          namespace: public # 这是默认值
          discovery:
            ephemeral: true # 是否是临时实例,若是false,则即使服务端超过30s没有收到心跳,也不会剔除,而是永久存在
            group: DEFAULT_GROUP # 默认值
            service: ${spring.application.name} # 默认值
            weight: 1 # 权重,默认值1
            namespace: public # 命名空间,默认值public
            access-key: "" # 访问秘钥,默认值空,只有部署在阿里云时才需要,上云就会用到
            secret-key: "" # 访问秘钥,默认值空,只有部署在阿里云时才需要
            watch:
              enabled: true # 是否开启心跳监听,默认值true
    		watch-delay: 30000 # 从服务端拉取实例的间隔时间,默认是30秒
            heart-beat-interval: 5000 # 心跳间隔,默认值5000
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    5.注册中心-核心功能总结

    • 服务注册:这是基础功能没什么说的,nacos会将注册的ip、端口等元数据信息存储在一个map中
    • 服务心跳:nacos客户端默认每5s向服务端发送一次心跳,防止被剔除
    • 服务同步:nacos集群间会相互同步实例,保证相互间信息一致性
    • 服务发现:nacos客户端调用服务提供者时,会向nacos服务端发送rest请求,以获取服务列表,并将其存储在本地,同时本地会定时更新缓存的服务列表
    • 服务健康检查:nacos服务端对于超过15秒没有做心跳的客户端会将其down,如果30秒仍未收到则会强制下线
      在这里插入图片描述
    • nacos注册中心工作流程
      • 1.nacos客户端启动时注册自己到达nacos服务
      • 2.nacos客户端定时从服务端拉取服务列表缓存到本地,定时通过MD5更新
      • 3.nacos客户端调用服务提供者时,是通过本地的注册表和负载均衡来调用远端服务的
      • 4.nacos客户端守护线程:默认5s一次心跳发送给服务端
      • 5.nacos服务端守护线程:默认15s收不到客户端心跳,将客户端状态置为false,30s收不到就会删除注册的实例

    三、Nacos注册中心集成Load Balancer 、OpenFeign

    1.Nacos客户端集成负载均衡Load Balancer

    在nacos2.0.3之前,他的客户端默认是集成了Ribbon的,且Ribbon的配置也是打开的,但是2.0.3之后就给关了。原因也是因为Ribbon已经闭源,作为Ribbon的开发者网非已经不对他进行更新。

    # 这是nacos2.0.3之后的默认配置,在这之前nacos是默认集成ribbon且使用ribbon的
    spring:
      cloud:
        loadbalancer:
          ribbon:
            enabled: false # 关闭ribbon    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    但是作为负载均衡组件,其实就是那几种负载策略,所以新的东西还是那几种(轮询、随机、权重、hash、最少连接),并且Ribbon的负载做的也挺好,为什么不接着更新呢,再写一个类似的感觉不是更消耗吗,而且现在Springcloud官方推荐的Load Balancer到现在为止也就只支持两种负载,还是最简单的轮询和随机。但是没得选啊,我们只能用Load Balancer了。

    • 1.第一步引入Load Balancer组件
      <!-- 引入loadbalancer负载均衡,父工程引入了cloud的包管理,这里无需声明具体包-->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
      </dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 2.在RestTemplate上增加@LoadBalancer注解
      因为默认情况下RestTemplate是没有实现的,所以需要我们手动生成,但是也无需提供任务参数,直接默认构造即可。
      import org.springframework.boot.web.client.RestTemplateBuilder;
      import org.springframework.cloud.client.loadbalancer.LoadBalanced;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.client.RestTemplate;
      
      /**
       * @author pcc
       */
      @Configuration
      public class SysConfig {
      
          /**
           * 创建RestTemplate
           * @param builder 构造器,这个restTemplate主要是测试使用
           *                加入feign后,将不在使用restTemplate进行测试
           * @return
           */
          @Bean
          @LoadBalanced
          public RestTemplate  getRestTemplate(RestTemplateBuilder builder) {
              return builder.build();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      这样其实基础使用就ok了,就可以实现通过nacos中的服务名来调用服务提供者了,同时还有负载均衡策略在里面。为什么加入LoadBalancer或者Ribbon就可以直接使用RestTemplate了呢?因为无论是LoadBalancer还是Ribbon都是属于客户端负载均衡的策略,而这类负载均衡都是通过将注册中心的实例拉取到本地以后才能进行计算负载的策略,所以我们增加了负载均衡时,他就会自动拉取我们的注册中心的实例,从而就认识了我们写的RestTemplate中的服务名了,如下:
      
      @RequestMapping("/addOrder")
      public void addOrder(){
          log.info("add order start ");
          // 只有增加了负载均衡,才能使用RestTemplate识别到注册中心的服务名
          restTemplate.getForObject("http://stock-server/stock/addStock",String.class);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • 3.更换LoadBalancer的负载策略
      LoadBalancer默认使用轮询的负载,我们这里更换为随机的负载(到目前为止,只支持两种)。我们需要自定义一个配置类,该配置类用以生产一个负载均衡的对象,然后将其交给@LoadBalancerClients注解,事实上如果我们不需要更换负载策略,这个配置(@LoadBalancer)是可以省略的,如下:
      import org.springframework.cloud.client.ServiceInstance;
      import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
      import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
      import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
      import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
      import org.springframework.context.annotation.Bean;
      import org.springframework.core.env.Environment;
      
      /**
       * @author pcc
       * 注意类上别加configuration注解
       */
      public class RandomLoadBalancerConfig {
      
          @Bean
          public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
              // 获取注册中心所有实例
              String property = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
              // 对实例进行负载
              return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(property, ServiceInstanceListSupplier.class),property);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      然后启动类上增加LoadBalancer的注解即可了
      import com.cheng.orderserver.config.RandomLoadBalancerConfig;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
      
      @LoadBalancerClients(defaultConfiguration= RandomLoadBalancerConfig.class)
      @SpringBootApplication
      public class OrderServerApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(OrderServerApplication.class, args);
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      以上就是更换负载的全部操作了,更改完成重启就会生效了。就使用上确实比Ribbon麻烦一些。不过无论是LoadBalancer还是Ribbon都是基于http的负载,这个通讯效率其实是不高的,在这个方面Dubbo是好很多的,这个不做过多研究(没必要,如果想研究可以去深研负载均衡算法)。

    2.Nacos客户端集成OpenFeign

    注册中心、负载均衡、远程调用,这是实现微服务的最基础的三个组件,使用了他们三个才能算是搭建了一个微型的服务,其他组件都是为了让服务更好的运行来设计的,只有他们三个是缺一不可,最基础的三个也是最没有看头的三个,nacos需要很少的配置就可以使用,至于LoadBalancer可以直接引入包就会默认使用。也就OpenFeign场景会多些。先说下基础使用

    • 1.引入jar包
      <!--引入OpenFign,父工程引入了cloud的包管理,这里无需声明具体包-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-openfeign</artifactId>
       </dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 2.写FeignClient接口类
      这个类可以当成是一个Controller来写,且必须和我们调用的接口的方法名、参数、返回等保持完全一致,否则无法调通,这里需要注意
      • name:传入被调用服务的名称,也就是注册中心中服务注册的名字
      • path:相当于类路径上的RequestMapping,可以不写,不写的话,方法上就声明全路径
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      /**
       * @author pcc
       * name:用以声明feign调用的服务,这个必须与注册中心服务名保持一致
       * path:控制器上的类路径,如果不写path在方法上写全路径也是一样的
       *
       */
      @FeignClient(name = "stock-server",path = "/stock")
      public interface StockFeignController {
      
          @RequestMapping("/addStock")
          void addStock();
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    • 3.启动类增加注解:@EnableFeignClients
      import com.cheng.orderserver.config.RandomLoadBalancerConfig;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
      import org.springframework.cloud.openfeign.EnableFeignClients;
      
      @EnableFeignClients
      // 不加自定义负载,这个配置就可以不要
      @LoadBalancerClients(defaultConfiguration= RandomLoadBalancerConfig.class)
      @SpringBootApplication
      public class OrderServerApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(OrderServerApplication.class, args);
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    • 4.使用:和普通的注入到Spring容器中的接口一样使用
      import com.cheng.orderserver.feign.StockFeignController;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      /**
       * @author pcc
       */
      @Slf4j
      @RestController
      @RequestMapping("/order")
      public class OrderController {
          @Autowired
          StockFeignController stockFeignController;
      
          @RequestMapping("/addOrder")
          public void addOrder(){
              log.info("add order start ");
              // 添加完feign以后就不需要restTemplate了
              stockFeignController.addStock();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      这样就实现了OpenFeign的简单使用,当然了这支持最基础的功能使用,下面来看一下略深一点的功能,也都是生产会用到的一些场景。

    3 OpenFien调用日志配置

    在开发或者调试过程中,有时候排查过程还是需要对Feign的调用过程进行打印的,因此掌握他的日志配置还是很有必要的。这里可以分为全局和局部两种配置。不过无论是使用全局还是使用局部都有一个前提,那就是更改Spring的默认日志级别,我们知道Spring的默认日志级别是info级别的。但是OpenFeign的日志打印的都是debug,所以我们必须调整Spring的展示日志级别我们才可以看到想看的日志,不过调整的话我们也是可以局部调整的,而不用全局调整。如下所示:

    # 日志级别配置
    # 使用feign日志时,必须要设置下面的配置,注意下面配置是Spring的,默认全局info,我们这里可以
    # 指定对应文件夹下的日志为debug,这样就不会全局debug了,这个配置指向哪个包,哪个包就是debug了
    # 也可以指向一个类
    logging:
      level:
    #    com.cheng.orderserver.feign: debug
        com.cheng: debug
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    加上这个配置后,指定包下面的日志界别就会变成debug了,注意是当前包下的所有日志都是以debug级别进行输出。下面看下OpenFeign如何设置全局和局部的日志配置。

    • 1.全局日志配置
      全局日志配置我们只需要在配置了Spring的日志(上面说的)的基础上增加一个配置类即可,如下:

      import feign.Logger;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author pcc
       */
      @Configuration
      public class FeignConfig {
      
          @Bean
          public Logger.Level getLoggerLevel() {
              return Logger.Level.FULL;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

      下面是结果:
      在这里插入图片描述

    • 2.局部日志配置-配置类
      若是感觉全局日志太多,我们可以只针对某一个feign调用类进行配置,直接使用上一步的配置文件去掉他的@Configuration注解(有这个注解就会全局生效),然后将其交给feign类的@FeignClient注解即可。如下:

      import feign.Logger;
      import org.springframework.context.annotation.Bean;
      
      /**
       * @author pcc
       */
      public class FeignConfig {
      
          @Bean
          public Logger.Level getLoggerLevel() {
              return Logger.Level.FULL;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

      下面是feign类配置

      import com.cheng.orderserver.config.FeignConfig;
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      /**
       * @author pcc
       * name:用以声明feign调用的服务,这个必须与注册中心服务名保持一致
       * path:控制器上的类路径,如果不写path在方法上写全路径也是一样的
       * configuration:配置类,可以配置一些公共的参数,只对当前类下的接口生效,属于局部配置
       *
       */
      @FeignClient(name = "stock-server",path = "/stock",configuration = FeignConfig.class)
      public interface StockFeignController {
      
          @RequestMapping("/addStock")
          void addStock();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    • 2.局部日志配置-配置文件
      使用配置文件的话看着更清晰明了一些。上面我们设置的都是 Logger.Level.FULL,这其实只是Feign日志的一种级别,Feign其实有四个级别

      • ① NONE:什么信息也不打,用了等于没用
      • ② BASIC:只打印基础信息,如下图
      • ③ HEADERS:只打印头部信息,如下图,线程场景中我们会在请求头传递一些token类的信息,打印出来还是有用的
      • ④ FULL:打印BASIC和HEADERS中的信息,如下图就是FULL的展示信息
        在这里插入图片描述

      这里的局部配置的配置文件配置我们就用BASIC进行展示:
      若是想要使用配置文件的方式配置全局只需要将服务名换成default,即可应用全局,后面的全局设置也是如此

      feign:
        client:
          config:
            stock-server: # 这里是被调用的服务名(注册中心里的名称)
              loggerLevel: BASIC # 这里使用BASIC,表示只打印请求方法名和URL,不打印请求头和响应头
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      在这里插入图片描述
      如上所示,就是OpenFeign的日志配置的全部内容了,一般调试信息或者排查问题是有用的。如果想要展示Feign的日志。也要严格限定只开启Feign目录下的debug日志,不然Spring提供的debug日志可是很多的,很容易把磁盘打满。此外还需要注意:若是既配置了全局又配置了局部那以谁为准呢?注意是以局部为准(很多框架都是这种局部配置大于全局)

    3 OpenFeign超时时间配置

    这里的配置也是分为全局和局部两种,其实和上面是类似的,全局的配置我们需要新增一个配置类(使用上面的全局配置类就行)就行了,如实局部配置有两种选择一种是和上面一样放到feign类的FeignClient注解里,一种是放到配置文件里,都差不多。超时时间配置支持两项一个是connetct,一个是read的。

    • 1.全局配置
      import feign.Logger;
      import feign.Request;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author pcc
       */
      @Configuration
      public class FeignConfig {
      
          /**
           *  设置feign的日志级别
           *
           *  FULL: 所有请求和响应
           *  HEADERS: 请求和响应的头信息
           *  BASIC: 请求方法、url、响应状态码和执行时间
           *  NONE: 默认,啥也没有
           * @return 日志级别
           */
          @Bean
          public Logger.Level getLoggerLevel() {
              return Logger.Level.FULL;
          }
      
          /**
           *
           * 设置feign的请求和响应的超时时间
           * @return 请求配置
           */
          @Bean
          public Request.Options options(){
              return new Request.Options(5000, 5000);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      此外服务提供者的接口增加一个线程睡眠6秒测试下,测试结果如下:
      在这里插入图片描述
    • 2 局部配置-配置类
      配置文件还是使用上面的文件,只需要把@Configuration去掉,然后把配置类交给feign类即可,这个和日志的配置是一样的,都是引用同一个配置文件
      import feign.Logger;
      import feign.Request;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author pcc
       */
      //@Configuration
      public class FeignConfig {
      
          /**
           *  设置feign的日志级别
           *
           *  FULL: 所有请求和响应
           *  HEADERS: 请求和响应的头信息
           *  BASIC: 请求方法、url、响应状态码和执行时间
           *  NONE: 默认,啥也没有
           * @return 日志级别
           */
          @Bean
          public Logger.Level getLoggerLevel() {
              return Logger.Level.FULL;
          }
      
          /**
           *
           * 设置feign的请求和响应的超时时间
           * @return 请求配置
           */
          @Bean
          public Request.Options options(){
              return new Request.Options(5000, 5000);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      然后我们在feign类上指明局部配置类,注意局部配置优先级大于全局,若同时配置的话,局部配置生效。
      import com.cheng.orderserver.config.FeignConfig;
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      /**
       * @author pcc
       * name:用以声明feign调用的服务,这个必须与注册中心服务名保持一致
       * path:控制器上的类路径,如果不写path在方法上写全路径也是一样的
       * configuration:配置类,可以配置一些公共的参数,只对当前类下的接口生效,属于局部配置
       *
       */
      @FeignClient(name = "stock-server",path = "/stock",configuration = FeignConfig.class)
      //@FeignClient(name = "stock-server",path = "/stock") // 取消feign的局部配置,使用配置文件
      public interface StockFeignController {
      
          @RequestMapping("/addStock")
          void addStock();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    • 2 局部配置-配置文件
      这种就不需要依赖配置文件了,只需要提供yml中的配置信息即可,如下:
      feign:
        client:
          config:
            stock-server: # 这里是被调用的服务名(注册中心里的名称)
              # 注意日志配置,必须配合Spring的日志配置才可以生效
              loggerLevel: BASIC # 这里使用BASIC,表示只打印请求方法名和URL,不打印请求头和响应头
              connectTimeout: 5000 # 连接超时时间,默认10s
              readTimeout: 5000 # 读取超时时间,默认60s
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      以上就是超时的配置了。

    4 OpenFeign自定义拦截器配置

    拦截器在java生态里都差不多,都是对请求或者响应进行处理的,这里是对OpenFeign的请求进行处理,我们可以增加一些日志信息,可以改变请求参数,可以增加请求头信息等等。在实际应用中可能会存在使用该拦截器往请求头里进行信息设置,从而追踪日志的调用链路的场景。因为分布式服务的调用日志时跨服务的,怎么确定日志是哪一条就可以通过这种方式来实现,当然市面上有比较好的解决方案,有时候不需要我们自己做,但原理都是类似的。这里同样是支持全局和局部配置的,而且与上面的日志、超时配置其实都差不多。

    • 1 全局配置
      先要自己实现一个Interceptor,自己的Interceptor需要实现OpenFeign的RequestInterceptor,重写他的apply方法,从这里可以看出OpenFeign底层还是RestTemplate。都是http请求。

      import feign.RequestInterceptor;
      import feign.RequestTemplate;
      import lombok.extern.slf4j.Slf4j;
      
      import java.util.UUID;
      
      /**
       * @author pcc
       */
      @Slf4j
      public class FeignInterceptor implements RequestInterceptor {
      
          @Override
          public void apply(RequestTemplate requestTemplate) {
              log.info("进入到了OpenFeign自定义拦截器了");
              requestTemplate.header("trace_id", UUID.randomUUID().toString());
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

      声明好拦截器以后,我们把他注入到Spring中就行,这样机会全局生效了。

      import com.cheng.orderserver.interceptor.FeignInterceptor;
      import feign.Logger;
      import feign.Request;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      /**
       * @author pcc
       */
      @Configuration
      public class FeignConfig {
      
          /**
           *  设置feign的日志级别
           *
           *  FULL: 所有请求和响应
           *  HEADERS: 请求和响应的头信息
           *  BASIC: 请求方法、url、响应状态码和执行时间
           *  NONE: 默认,啥也没有
           * @return 日志级别
           */
          @Bean
          public Logger.Level getLoggerLevel() {
              return Logger.Level.FULL;
          }
      
          /**
           *
           * 设置feign的请求和响应的超时时间
           * @return 请求配置
           */
          @Bean
          public Request.Options options(){
              return new Request.Options(5000, 5000);
          }
      
          /**
           * 配置feign的拦截器
           * 将其交给Spring即可
           */
          @Bean
          public FeignInterceptor feignInterceptor() {
              return new FeignInterceptor();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45

      这样就会对所有的feign接口进行拦截了,如下截图:
      在这里插入图片描述

    • 2 局部配置-配置类
      这里和上面两种说的局部配置没有区别,就不重复演示了:将配置类取消@Configuration注解,然后使用FeignClient指明配置类即可。

    • 2 局部配置-配置文件
      自己写拦截器这一步是无法省略的,然后我们可以将拦截器交给指定的服务,这样就只会拦截指定的服务了。

      feign:
        client:
          config:
            stock-server: # 这里是被调用的服务名(注册中心里的名称)
              # 注意日志配置,必须配合Spring的日志配置才可以生效
              loggerLevel: BASIC # 这里使用BASIC,表示只打印请求方法名和URL,不打印请求头和响应头
              connectTimeout: 5000 # 连接超时时间,默认10s
              readTimeout: 5000 # 读取超时时间,默认60s
              requestInterceptors: # 请求拦截器,可以添加多个
                - com.cheng.orderserver.interceptor.FeignInterceptor # 这里指定的是拦截器的全限定名
           
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      像下面这样配置也是可以的:

      feign:
        client:
          config:
            stock-server: # 这里是被调用的服务名(注册中心里的名称)
              # 注意日志配置,必须配合Spring的日志配置才可以生效
              loggerLevel: BASIC # 这里使用BASIC,表示只打印请求方法名和URL,不打印请求头和响应头
              connectTimeout: 5000 # 连接超时时间,默认10s
              readTimeout: 5000 # 读取超时时间,默认60s
              requestInterceptors[0]: # 请求拦截器,可以添加多个
                com.cheng.orderserver.interceptor.FeignInterceptor # 这里指定的是拦截器的全限定名
           
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      以上都是openfeign的扩展功能了,在性能等上面其实没啥影响。都是些辅助功能。

    4 OpenFeign整合Sentinel

    这里OpenFeign整合Sentinel只为了展示服务降级的处理,不做其他功能的延伸。OpenFeign整合Sentinel只需要两小步即可。

    • 1 导入Sentinel的依赖
      <!--sentinel依赖-->
      <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
      </dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 2 开启Sentinel对OpenFeign的支持
      在yml或者properties中配置即可
      # 已省略关系不大的配置
      feign:
        sentinel:
          enabled: true # 开启sentinel对feign的支持
      
      • 1
      • 2
      • 3
      • 4
      我们无需为Sentinel增加额外的配置就可以直接使用了

    5 OpenFeign定义服务降级

    OpenFeign支持两种服务降级的方式,不过需要整合Sentinel或者Hystrix,这里以整合Sentinel为例

    • 1 FeignClient中使用fallback指定降级类
    • 2 FeignClient中使用fallbackFactory指定降级类

    既然支持了两种,那他们肯定有区别,最主要的区别是fallback获取不到异常,而fallbackFactory是可以知道具体异常的,因为知道了具体异常就可以根据不同异常做不同的处理,所以真实场景中大部分是使用fallbackFactory(也有少量用fallback的)。下面对他们进行详细说明:

      1. fallback服务降级
        使用fallback必须满足以下条件
        1. fallback指定的类必须交给Spring容器管理
        1. fallback指定的类必须实现FeignClient修饰的feign接口

      如此我们就可以将类交给fallback了,这是一旦有异常发生,就会引入到我们重写的对应方法中了。下面是测试代码:
      这是feign接口代码:

      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      /**
       * @author pcc
       * url: 用于指定三方接口,通过url指定的接口不会从注册中心获取服务列表,而是直接通过接口名拼接地址
       * name:用以声明feign调用的服务,这个必须与注册中心服务名保持一致
       * path:控制器上的类路径,如果不写path在方法上写全路径也是一样的
       * configuration:配置类,可以配置一些公共的参数,只对当前类下的接口生效,属于局部配置
       * fallback : 声明降级类
       *
       */
      @FeignClient(name = "stock-server",path = "/stock",
              fallback = StockFeignControllerDegrade.class)
      public interface StockFeignController {
      
          @RequestMapping("/addStock")
          void addStock();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

      下面是降级类的代码,很简单,就是无法知道因何种异常导致的服务降级

      import lombok.extern.slf4j.Slf4j;
      import org.springframework.stereotype.Component;
      
      /**
       * @author pcc
       */
      @Slf4j
      @Component
      public class StockFeignControllerDegrade implements StockFeignController{
          @Override
          public void addStock() {
              log.info("addStock: 降级方法处理中。。。");
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

      然后我们来测试下,可以发现服务端异常时正常进入到了降级方法内部(服务提供方代码就是抛出了一个异常)。
      在这里插入图片描述

      1. fallbackFactory服务降级
        使用fallbackFactory需要遵循以下规则
      • 1 实现类需要交给Spring管理
      • 2 实现类需要实现FallbackFactory接口并传入Feign接口的泛型

      以下是Feign接口的代码

      /**
       * @author pcc
       * url: 用于指定三方接口,通过url指定的接口不会从注册中心获取服务列表,而是直接通过接口名拼接地址
       * name:用以声明feign调用的服务,这个必须与注册中心服务名保持一致
       * path:控制器上的类路径,如果不写path在方法上写全路径也是一样的
       * configuration:配置类,可以配置一些公共的参数,只对当前类下的接口生效,属于局部配置
       * fallback : 声明降级类
       * fallbackFactory: 声明降级工厂类
       *
       */
      @FeignClient(name = "stock-server",path = "/stock",
              fallbackFactory = StockFeignCallBackFactory.class)
      public interface StockFeignController {
      
          @RequestMapping("/addStock")
          void addStock();
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

      下面是降级工厂实现类

      import lombok.extern.slf4j.Slf4j;
      import org.springframework.cloud.openfeign.FallbackFactory;
      import org.springframework.stereotype.Component;
      
      /**
       * @author pcc
       */
      @Slf4j
      @Component
      public class StockFeignCallBackFactory implements FallbackFactory<StockFeignController> {
          @Override
          public StockFeignController create(final Throwable cause) {
              return new StockFeignController() {
                  @Override
                  public void addStock() {
                      log.info("addStock:fallbackFactroy降级中:{}",cause.getMessage());
                  }
              };
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20

      上面代码为了演示异常的明细,把Throwable cause 改成了 final Throwable cause,不然内部类无法操作变量cause(经常写lambda这个问题应该不会陌生)。执行后可以看到下图中我们在降级方法中拿到了远端接口的异常信息。
      在这里插入图片描述

    6.服务降级与全局统一异常处理

    在Cloud的早期版本服务降级是只针对DegradeException进行处理的,其他异常是不会进入降级方法的,现在默认所有异常均可以进入降级方法,那一旦进入降级方法其实就会无法被统一异常进行捕获,所以有时候需要我们进行权衡,哪些异常需要我们作降级处理,哪些异常则不需要我们做异常处理。当然我们直接使用降级方法代替统一异常也是可以的,看自己的选择了。统一异常处理可以看成是一个兜底的方案。

    7.全局异常处理与降级方法

    若使用OpenFeign进行远程调用时,远端服务一旦使用了全局异常处理,就会导致消费方获取不到这个异常,从而无法进入到正常的消费方的降级方法内部(Feign客户端需要异常的http状态码才可以进入到降级方法内部)。
    有如下常用的解决方法:

    • 1.服务方在统一异常中定义指定的返回码,通过返回码消费方来进行指定的动作,这样消费方的降级方法也不会触发,不过可以处理异常场景了。
    • 2.服务端的全局异常处理,排除指定包下的异常,也就是说指定某些包下的异常不归全局异常处理管。
      假如有以下全局异常处理代码:
      import com.ebbing.util.Response;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.http.HttpStatus;
      import org.springframework.validation.ObjectError;
      import org.springframework.web.bind.MethodArgumentNotValidException;
      import org.springframework.web.bind.annotation.ExceptionHandler;
      import org.springframework.web.bind.annotation.ResponseStatus;
      import org.springframework.web.bind.annotation.RestControllerAdvice;
      
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.util.List;
      import java.util.StringJoiner;
      
      /**
       * @author pcc
       */
      @Slf4j
      @RestControllerAdvice
      public class GlobalException {
      
          @ResponseStatus(HttpStatus.OK)
          @ExceptionHandler
          public Response<Object> validatedExceptionHandler(MethodArgumentNotValidException e, HttpServletRequest request, HttpServletResponse response) {
              log.info("全局异常捕获异常:{}",e.getMessage());
      
              StringJoiner errorMsg = new StringJoiner(",","","!");
              List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
              allErrors.forEach(objectError -> {
                  errorMsg.add(objectError.getDefaultMessage());
              });
      
              return Response.fail(errorMsg.toString());
          }
      
      
          @ResponseStatus(HttpStatus.OK)
          @ExceptionHandler
          public Response<Object> validatedExceptionHandler(Exception e, HttpServletRequest request, HttpServletResponse response) {
              log.info("全局异常捕获异常:{}",e.getMessage());
              return Response.fail("系统异常请联系管理员");
          }
      
      
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      可以通过如下方式进行排除指定包下的异常不归全局异常管理:
      import com.ebbing.util.Response;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.core.Ordered;
      import org.springframework.core.annotation.Order;
      import org.springframework.web.bind.annotation.ExceptionHandler;
      import org.springframework.web.bind.annotation.RestControllerAdvice;
      
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      
      /**
       * @author pcc
       */
      @Slf4j
      @Order(Ordered.HIGHEST_PRECEDENCE)
      @RestControllerAdvice(basePackages = {"com.ebbing.task.controller"})
      public class GlobalFeignException {
      
          @ExceptionHandler
          public Response<Object> validatedExceptionHandler(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {
              throw e;
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      以上代码有两点需要注意:
      ①必须增加Order注解且,声明当前的异常处理为更高优先级(值越小优先级越高),为全局异常处理增加一个Order,设置优先级较低(这里没加)
      ②RestControllerAdvice注解给属性basePackages赋值,值是字符串数组,声明哪些包不需要处理即可。
      这样就可以实现服务端的异常可以正常传递到客户端了。

    8.如何实现OpenFeign调用本地服务

    8.1 使用FeignClient中的URL

    使用FeignClient中的url属性指定ip和端口即可,此时name也必须声明,且name值必须是正确的eureka或者nacos中的服务,因为即使我们声明了url。OpenFeign在找服务时也是根据name的地址列表来找我们配置的url,找到了才可以正常请求。相当于是我们指定当前的服务调用的是注册中心中的某一个固定的服务提供者。示例如下:

    @FeignClient(
            url = "172.17.65.66:8021",
            name = ServiceNameConstants.PROVIDER_SERVICE, fallbackFactory = FeignXXXFallbackFactory.class)
    public interface FeignXXXService {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样我摸嗯就可以保证只调用本地的服务了。需要特别注意的点:

    • 1 url必须是ip+端口,即使本地也不能用localhost,因为url是需要和注册中心的服务列表进行比对的,比对上了才可以使用url
    • 2 name必须是真实的注册中中心中的服务名

    8.2 直接更改服务提供者的name,然后更改FeignClient的name

    还有一种调试本地的方案:直接更改服务提供者的name,当然对应的FeignClient中的name也要改。

    三、Nacos配置中心

    Nacos的服务端都是共用的,这里就不重复进行展示了,只说下配置中心客户端的具体情况

    1.配置中心-客户端搭建

    客户端搭建差不多,都是三步走:导入依赖,配置文件,使用。基本都差不多,当然这里需要我们配置远端服务了。

    • 1.导入依赖
      这里依赖包注意是两个,一个是nacos-config的,一个是bootstrap的,nacos-config的配置必须配置在bootstrap中,否则不生效。所以我们需要导入两个包。注意要确认包导入完毕了,不然启动肯定会报错找不到要解析的配置信息。比如使用@Value进行注入配置,会告诉你这个配置信息解析不了。

      
      <dependency>
          <groupId>com.alibaba.cloudgroupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
      dependency>
      
      
      <dependency>
          <groupId>org.springframework.cloudgroupId>
          <artifactId>spring-cloud-starter-bootstrapartifactId>
      dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 2.增加配置文件bootstrap.yml
      添加如下配置,这个配置其实在配置nacos配置中心时其实已经写过了,与application.yml一样,没有任何区别,但是放到application.yml的话配置是不生效的。下面是直接从applicaion.yml复制过来的配置,无需任何改动就可以使用

      spring:
        application:
          name: order-server # nacos配置中心和注册中心默认都是用这个作为默认名称
        cloud:
          nacos:
            # 这里配置集群时,启动时默认只会连接其中一个,但是若是有节点挂掉,可以自动切换到另外的结点
            # 这里只配置一个,不然虚拟机ip一变(没有配置静态ip),集群多个结点就相互注册不到对方了,所以用一个结点
            #      server-addr: 192.168.150.187:8848,192.168.150.188:8848
            server-addr: 192.168.150.187:8848
            user-name: nacos # 这是默认值
            password: nacos # 这是默认值
            namespace: public # 这是默认值
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 3.配置中心增加配置文件
      这里需要注意:

      • 默认配置文件的后缀是properties,若是不增加配置,在服务端必须选择properties的类型配置文件
      • 不增加额外配置的话,nacos默认的配置文件名是:其中prefix默认取得是spring.application.name,spring.profiles.active就是springboot支持的多文件配置,不写的话默认就是空,file-extension是nacos扩展名也就是文件后缀名的配置,默认是properties。所以当我们不添加任何配置时默认是“服务名.properties”作为配置文件名(spring.profiles.active不配置的话会包含前面的-一起省略)。官方文档参考:naocs官方文档-springcloud
        ${prefix}-${spring.profiles.active}.${file-extension}
        
        • 1

      所以这里我们在nacos配置中心的文件名应该叫:order-server。文件类型选择properties或其他类型其实没有关系,file-extension限定的是dataid的名字,和文件的具体类型是没有关系的。
      在这里插入图片描述
      编辑完发布即可。

    • 4.使用nacos配置信息
      这里就是使用了,使用的话还是很简单的,和application.yml使用没有任何区别。这里使用@Value进行注入验证

      import com.cheng.orderserver.feign.StockFeignController;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      /**
       * @author pcc
       */
      @Slf4j
      @RestController
      @RequestMapping("/order")
      public class OrderController {
      
          @Autowired
          StockFeignController stockFeignController;
      
          @Value("${name}")
          String name;
      
          @RequestMapping("/addOrder")
          public void addOrder(){
              log.info("add order start :{}",name);
              stockFeignController.addStock();
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27

      下面是接口输出截图:
      在这里插入图片描述
      到这里客户端的基础搭建就ok了,我们已经可以读取到配置中心的文件了。然后就可以将本地的配置信息同步到nacos配置中心了。

    2.配置中心-管理页面

    其实都是些基础功能,需要说的也不是很多

    • 1.Data Id
      data id就是配置文件的名称,客户端默认是spring.application.name 也可以手动指定。不指定默认就是spring.application.name,一般都是使用默认,没人去更改这个。

    • 2.namespace 和 group
      这个和注册中心的概念是一样的,都是为了做区分的,怎么使用都是看自己,一般场景使用namespace作为环境隔离,使用group作为应用隔离,dataid 作为服务隔离。不过实际场景中很少有人将生产和测试使用同一套nacos。所以命名空间一般都是不使用,或者就是建造一个自己用的命名空间。group就要看情况了,若是一个中台有很多应用。是可以使用group进行区分应用的,再用dataid来区分同应用下的不同服务。

    • 3.编辑、克隆、导出、导入
      这些都是基本配置,可以更方便的进行配置信息的管理,没啥好说的

    • 4.历史版本
      这个可以对文件的历史版本 回溯,并还原历史版本。类似git的功能。这个还是有用的
      在这里插入图片描述

    • 5.监听查询
      这里需要注意,并不是说有服务在使用这个配置文件,监听查询中就可以看到对应服务的,测试服务启动和配置变更时都会服务均未正常出现在监听列表内,可能是bug。

    3.配置中心常用配置

    nacos有两个默认配置文件,也就是说我们不添加任何配置的话,nacos也会去服务端寻找这两个配置文件进行加载(在指定的命名空间和分组下)。注意这里指的都是服务端的文件名,和文件是yml配置文件还是properties配置文件没有任何关系。那是哪两个呢?

    • 1.${spring.application.name}
    • 2.${spring.application.name}.${file-extension}

    若是我们服务名是order-server,则nacos客户端默认会去服务端加载这两个配置文件:order-server、order-server.properties,注意这里是找的是文件名(data id),这里的properties是指文件名的后缀,与文件的配置格式是不是properties没有关系,也就是说你可以在nacos服务端起个order-server.properties的名字(dataid)然后里面是yml配置文件,这也是ok的。虽然可以但是别这么干,很容易让接收项目的兄弟吐槽你不专业,一般保持一致方便区分。特别需要注意的是若是在order-server、order-server.properties都添加了相同的配置,则使用的是order-server.properties的配置。order-server.properties的优先级更高。
    下面是不添加任何配置时服务启动的日志:
    在这里插入图片描述
    可以看到nacos为我们默认加载这两个文件,如果加载不到也不会报错的,只是会在之前的日志里提示空文件。那这个默认后缀properties是如何来的呢,请看下面 3.1

    3.1 文件扩展名配置:file-extension

    服务端默认使用properties作为服务扩展名,若是想要变更则客户端必须进行指定,指定的话使用该配置进行指定,下面是不增加这个配置时的客户端启动的日志
    在这里插入图片描述
    在这里插入图片描述

    可以发现,nacos客户端会扫描两个服务端的配置文件,一个是order-server,另一个是order-server.properties。下面新增file-extension的配置,注意位置,此时切记不可以在config下重新声明namespace,这会导致无法正常寻找到配置文件。namespace和user-name、password都应该声明在nacos下,而不是config下。此外还需要注意,file-extension修改的是dataid的名字与文件内部是yml和properties都没有关系,内部文件的类型我们可以随便指定,但是一般为了可维护性更高会保持一致。

    # 已省略与此处关系不大的配置
    spring:
      cloud:
        nacos:
          config:
    #        namespace: public # 这里不可以重复这么些,写了就无法正常加载配置文件
            file-extension: yaml # 默认是properties,若是yaml,需要配置这个
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里我虽然增加了file-extension的配置,但是我在服务端并没有增加这个文件,所以提示加载不到。当我们提供了这个文件后就不会有这个提示了在这里插入图片描述 这个日志文件告诉我们nacos客户端会从哪些服务端配置文件加载配置:在这里插入图片描述
    注意这个配置是针对默认配置文件${spring.application.name}.${file-extension}和${spring.application.name}-${spring.profiles.active}.${file-extension}有用,若是使用指定dataid方式进行加载时则无需遵循这个规则(这个后面3.5细说)。
    特別注意:查阅资料说config下支持namesapce的配置,但是亲测这里配置命名空间以后服务是无法读取到对应的配置信息的,所以若是需要更改namespace还是在nacos配置属性中更改,不要在config中更改,这里nacos客户端:2.0.4

    3.2 使用profile进行多环境文件配置

    这是何种场景下使用的呢?若是对所有配置文件不区分环境都放在了一起,就像写SpringBoot项目时不使用配置中心一样,此时我们需要声明

    • application-dev.yml
    • application-uat.yml
    • application-prd.yml

    多个环境的不同文件,然后使用

    spring:
      profiles:
        active: dev # 这个配置声明在application和bootstrap都是可以的,bootstrap优先级高
    
    • 1
    • 2
    • 3

    这个配置来指明当前使用的到底是哪个文件,但是使用nacos完全没有必要这么写的。一般不同环境的配置文件在nacos服务端是不会放一起的,都是使用不同的nacos集群。所以这个场景一般不这么用,不过nacos是支持这种配置的,以防止有些人就是想要放一起。前面说过nacos加载的配置文件默认是:

    ${prefix}-${spring.profiles.active}.${file-extension}
    
    • 1

    当我们增加了如上的配置以后:nacos默认会读取以下三个配置文件,假设file-extension是yaml:

    • order-server
      这是默认读取,也是nacos默认的配置文件,无需任何配置会直接读取该配置文件
    • order-server.yaml
      这个也会默认读取,注意文件后缀
    • order-server-dev.yaml
      若是新增了profile的配置就会根据环境读取对应的配置文件了,这里dataid必须完全匹配才可以包括后缀,所以服务端应该有这个文件:
      在这里插入图片描述

    重启服务,验证下是不是读的这些配置文件:
    在这里插入图片描述
    可以看到正常去加载这些配置文件了,配置文件的使用都是一样的,就不具体说了,唯一需要特殊说明的是他们的配置优先级。在Springcloud中,bootstrap>application-profile>application,这里基本是一样的。他们三个是
    order-server-dev.yaml > order-server.yaml > order-server
    建议:不要这么使用,因为nacos一般根据集群区分环境,而不是都放到一个nacos服务端,这样不是好的配置文件管理方式。那我们该怎么管理不同的环境下不同的地址呢?我们一般是通过本地的profile配置文件来管理,而不是远端放到一起。假如我们存在三个环境dev、uat、prd。则我们需要本地提供这么三个文件:

    • bootstrap-dev.yml
    • bootstrap-uat.yml
    • bootstrap-prd.yml

    这里需要注意三点:

    • 1.为什么使用bootstrap-dev.yml而不是使用application-dev.yml
      因为nacos配置中心客户端的信息必须配置在bootstrap中,配置在application中不会生效,所以必须使用bootstrap-dev.yml其他同理。
    • 2.我们不同环境地址放到对应的文件中即可
      不同环境的服务启动服务时可以通过传入profile的参数来指定使用的配置文件(或者将这个信息配置到服务器上也是可以的),传入的配置优先级大于配置文件中默认指定的。如下:
      # 使用-- 来覆盖配置信息,使用-- 必须参数放在jar后面
      java -jar ./order-server.jar --spring.profiles.active=dev
      # 使用-D 来覆盖配置信息,这种写法优先级更高,且位置没有要求
      java -jar -Dspring.profiles.active=dev ./order-server.jar
      
      • 1
      • 2
      • 3
      • 4
    • 3.bootstrap-dev.yml等文件中不可重复声明spring.profiles.active
      这里不可以重复声明,否则SpringBoot无法正常加载配置

    3.3 配置更新禁用

    nacos客户端默认每10ms去服务端查看下配置有无变更,通过比对MD5的值来进行校验。变更的话就会进行重新拉取。这个操作默认是开启的,使用nacos的原因一部分也是因为他的动态感知能力,所以一般不会有人去禁用这个的,除非是一些服务配置不希望随便被更改的,这些都是些特殊场景了。

    # 已省略关系不大配置
    spring:
      cloud:
        nacos:
          config:
    #        namespace: public # 这里不可以重复这么些,写了就无法正常加载配置文件
            file-extension: yaml # 默认是properties,若是yaml,需要配置这个
            refresh-enabled: true # 默认是true,nacos每10ms去服务端比对信息有误变化
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这个场景想要验证需要配合@RefreshScope注解来实现,这个注解下面会详细说下,他的作用就是将使用了配置中心的属性或者对象放到一个context中,一旦配置中心改变就会热加载这个配置。当上面的配置未配置时,我们使用这个@RefreshScope是可以是实现热加载的,但是加了refresh-enabled:false以后就不会有热加载了。原因就是因为nacos客户端不会再每10ms去服务端比对信息变化了。

    3.4 config的namespace和group

    查阅资料说config下支持namesapce的配置,但是亲测这里配置命名空间以后服务是无法读取到对应的配置信息的,所以若是需要更改namespace还是在nacos配置属性中更改,不要在config中更改,这里nacos客户端:2.0.4

    spring:
      cloud:
        nacos:
          namespace: dev # 这是默认值
          config:
    #        namespace: dev # 这里不可以重复这么些,写了就无法正常加载配置文件
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    若是在config中进行更改namespace则无法正常加载配置信息。不过在config中声明group却是可以正常加载的。nacos服务端允许同namespace下存在不同group的同名文件。
    在这里插入图片描述
    这种是可以正常加载到配置信息的。

    3.5 指定配置文件: shared-configs

    项目中肯定会碰到有一些公共配置信息是需要我们进行共享的,对于这部分信息若是在每个配置文件都配置一遍则是很low的一个动作,而且一旦发生变更我们需要每个配置文件都去改一下,一旦漏了一个就是事故了。所以主流的配置中心都有这个功能就是共享配置文件,用以配置一些所有服务或者多个服务共享的信息。nacos里可以通过shared-configs来进行配置共享信息。注意:

    • 1.shared-configs的文件名后缀不受上面说的file-extension配置的限制,可以自由指定
    • 2.shared-configs的group默认是DEFAULT_GROUP,也不受config配置下的group的限制,虽然这个shared-configs放在了config下。
    • 3.shared-configs默认不动态加载服务端变更,必须更改默认值为true
    • 4.shared-configs支持多个默认配置文件,多个默认配置文件的加载顺序是按照声明的顺序加载的,若是多个文件存在相同配置,则后加载的会进行覆盖之前已经加载的信息
    • 5.shared-configs的优先级很低,假如服务是order-server优先级是这样的:bootstrap>order-server-dev.yml>order-server.yml>order-server>shared-configs

    假如有一个公共的配置文件叫:common.yaml ,应该像如下进行配置:

    # 已省略关系不大的配置
    spring:
      cloud:
        nacos:
          config:
            shared-configs:
              - data-id: common.yaml # 这里配置多个,可以配置多个配置文件,但是要保证文件名不能重复
                refresh: true # 默认是fase,也就是不会动态去刷新配置文件
                group: DEFAULT_GROUP # 默认值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    shared-configs是一个数组,Springboot中数组的配置支持两种,一种是- 来进行配置元素,一种是使用下标来配置来配置,使用下标应该这么写:

    spring:
      cloud:
        nacos:
          config:
            shared-configs[0]:
              data-id: common.yaml # 这里配置多个,可以配置多个配置文件,但是要保证文件名不能重复
              refresh: true # 默认是fase,也就是不会动态去刷新配置文件
              group: DEFAULT_GROUP # 默认值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    使用验证与之前都没有区别,不重复举例了。

    3.5 指定配置文件: extension-configs

    extension-configs和shared-configs基本一样,包括上面说的shared-configs的内容都适用于extension-configs。他们区别主要是加载优先级不一样。加载优先级 extension-configs > shared-configs 。而 extension-configs 与其他文件相比同样是最低的。
    使用和shared-configs没有任何区别,这里简单举例:

    # 已省略关系不大的配置
    spring:
      cloud:
        nacos:
          config:
            shared-configs[0]:
              data-id: common.yaml # 这里配置多个,可以配置多个配置文件,但是要保证文件名不能重复
              refresh: true # 默认是fase,也就是不会动态去刷新配置文件
              group: DEFAULT_GROUP # 默认值
            extension-configs:
              - data-id: common2.yaml # 同样也可以配置多个,但是要保证文件名不能重复
                refresh: true # 默认是fase,也就是不会动态去刷新配置文件
                group: DEFAULT_GROUP # 默认值
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.6 配置文件变动热加载:@RefreshScope

    配置文件配置的信息,他的变化是可以被服务实时感知到的,前面也说了nacos客户端每10ms会去服务端做一次配置文件MD5的比对,若是发现变更了,则会从新拉取配置信息到本地。但是我们虽然拉到本地了,但是Spring加载信息的动作只会执行一次,所以我们一般使用@Value获取的信息并无法直接随着我们的变更而变更。此时我们就需要@RefreshScope注解了

    • RefreshScope用于类上:整个类下从nacos获取的配置信息都会动态更新
    • RefreshScope用于方法:动态刷新的范围是方法
      使用的话也很简单,直接放在类上或者方法上即可。
    @Slf4j
    @RestController
    @RequestMapping("/order")
    @RefreshScope
    public class OrderController {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如此,当nacos服务端配置变动时信息就会实时更新到@Value上了。RefreshScope是config提供的注解,而不是nacos的,nacos是按照这一标准进行实现了。

    四、总结和问题

    Nacos作为当下最流行的注册中心和配置中心的解决方案,是必须掌握的一项技能,本文旨在总结基础的使用,从Nacos注册中心开始,继承RestTemplate实现负载,然后使用OpenFeign代替RestTemplate等。后面对配置中心的使用进行了进一步总结,比较需要关注的是多文件配置以及共享文件配置的shared-configs、extension-configs以及支持动态刷新配置到已加载配置的变量或者对象中的配置@RefreshScope。

    1.配置file-extension以后,无法正常加载默认文件(偶现)

    nacos会默认加载配置文件

    • ${spirng.application.name}
    • ${spring.application.name}.${file-extension}

    假如服务是order-servere,且不指定file-extension时,默认加载的配置文件是:order-server、order-server.properties这俩配置文件。当增加file-extension配置如下:

    file-extension: yaml
    
    • 1

    此时默认应该是读取配置文件:order-server、order-server.yaml,此时若是没有再nacos服务端新建配置文件order-server.yaml有时会提示我们已经在order-server中配置的信息加载不到,而且该错误不是必现,有时候会出现。笔者把配置信息file-extension注释掉从新启动就没有问题,来回验证了至少3遍。发现确实是file-extension影响的。不过隔天以后再去验证就没了这个问题,很奇怪,在此记录下。如果碰到配置信息都没有问题的情况下可以往这个场景下试试。
    这是问题记录:

    注意:若是增加了该配置file-extension: yaml
    - 未配置spring.profiles.active:nacos服务中必须有配置文件order-server.yaml,有order-server也不行,必须有他order-server.yaml,否则服务启动报错,会告诉我们无法正常加载配置文件中的属性,即使这个属性在order-server中有也加载不到,若是没有配置file-extension,是可以直接使用order-server这个默认配置文件的
    - ==配置了spring.profiles.active:nacos服务中必须有配置文件order-server-dev.yaml(假设环境选择是dev),若是没有也无法正常启动,和上面相同,有order-server也是不好使的
    
    • 1
    • 2
    • 3

    结论:
    为了防止上面这种情况出现(偶现的才最坑爹啊)我们最好提供配置以下配置文件在服务端,防止异常:

    • ${spring.application.name}-${spring.profiles.active}.${file-extension}
  • 相关阅读:
    PyCharm+PyQT5之二第一个QT程序
    Docker安装——Ubuntu (Jammy 22.04)
    用户如何选择正规的证券交易量化接口?
    ImmunoChemistry艾美捷基本细胞毒性试验试剂盒测定方案
    FITC-PSA豌豆凝集素,PSA-FITC,豌豆凝集素修饰绿色荧光素
    Spring boot项目集成阿里云短信服务发送短信验证码
    cache操作:clean、invalidate与flush的含义
    541、RabbitMQ详细入门教程系列 -【Jackson2JsonMessageConvert】 2022.09.05
    JAVA:实现字符串WordLadder字梯算法(附完整源码)
    轨道交通上的安科瑞精密配电多回路监控装置
  • 原文地址:https://blog.csdn.net/m0_46897923/article/details/132639913