单体架构VS微服务
单体架构
单体架构,项目的所有功能模块都在同一个工程中,将一个系统作为一个整体来进行开发、打包、部署、运行,这种架构开发简单,成本低,上手快,易于部署和测试,但是当开发规模越来越大,各项业务越来越复杂的时候就会呈现出各种问题。
各模块之间耦合较大,团队沟通成本高,难以维护扩展。
项目中某个模块有修改时需要重新部署整个系统,耗时长。
各模块之间会相互影响,热点资源会消耗大量资源,影响其他模块。
微服务架构
微服务架构将独立的功能模块作为单个小服务,各个服务可以独立的运行,部署,有效的降低了模块之间的耦合度,降低了沟通成本,单个服务修改后只需重新部署该服务即可,易于升级维护,各个服务部署在自己的服务器上,不会影响到其他服务。解决了单体架构中遇到的问题,但是新的问题随之出现。
如何管理众多微服务?
跨服务业务如何处理?
各个服务部署在不同的服务器上,客户端如何访问?
服务调用
RestTemplate
RestTemplate是Spring提供的可以实现Http请求发送的API,可以通过服务之间发送网络请求来实现服务调用。
首先,注入RestTemplate到Spring容器
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
发起请求
public <T> ResponseEntity<T> exchange(
String url, //请求路径
HttpMethod method, //请求方式
@Nullable HttpEntity<?> requestEntity, //请求实体
Class<T> responseType, //返回值类型 或ParameterizedTypeReference<T> responseType
Object... uriVariables //请求参数 或Map<String, ?> uriVariables
)
其中,返回值类型如果是不涉及到泛型,如User,可以传User.class,如果涉及到了泛型,如List<User>,不可以传List<User>.class,因为字节码中是没有泛型的,泛型会被擦除。这时需使用ParameterizedTypeReference,是Spring中的一个泛型类,处理带有泛型参数的类型,避免出现类型擦除的问题。直接new ParameterizedTypeReference<List<User>>() {}。
ResponseEntity<List<User>> response = restTemplate.exchange(
"http://localhost:8081/users?ids={ids}",
HttpMethod.GET, //请求方式
null, //请求实体
new ParameterizedTypeReference<List<User>>() {}, //返回值类型
Map.of("ids", CollUtils.join(userIds, ",")) //请求参数
);
方法返回一个ResponseEntity对象,可以调用getStatusCode()、getBody()等方法解析响应结果进一步处理。
if (!response.getStatusCode().is2xxSuccessful()){
//请求失败,返回
return;
}
List<User> users= response.getBody();
这样就实现了服务之间的远程调用。
但是这种方法存在一些问题:如果某个服务部署了多个实例在不同的服务器不同的端口上,我们是在编码时是不知道具体有多少个实例,又分别是什么IP地址什么端口的;不能实现负载均衡的效果;如果有服务挂了怎么办?如果又增加了新的实例怎么办?这就涉及到了新的知识--服务治理。
服务治理
注册中心
注册中心涉及到三种角色:服务提供者、服务调用者、注册中心。
服务提供者和服务调用者是相对的,并不是谁一定是提供者谁一定是调用者。
注册中心原理
当服务启动时,都会区注册中心注册自己的服务信息,包括服务名、ip和端口等信息,当有调用者调用其他服务时,向注册中心订阅服务提供者的信息,注册中心向其返回所需服务的实例表,然后再通过负载均衡从多个实例中选出一个进行调用。
服务实例与注册中心有心跳续约机制,实例每隔一定时间会向注册中心报告自己的状态,如果有实例挂了,那么其无法向注册中心续约,注册中心会认为该实例可能宕机而在该服务的实例表中将其剔除,并向订阅了该服务的服务推送变更信息。
当有新的服务实例部署时,启动后也会向注册中心注册,注册中心变更该服务实例表并推送变更信息。
Nacos
Nacos是阿里巴巴推出的开源项目,是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Nacos部署
Nacos官网有详细步骤,我这边步骤如下:
准备Mysql数据表存储Nacos数据
drop DATABASE if EXISTS nacos;
CREATE DATABASE nacos CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
use nacos;
/******************************************/
/* 表名称 = 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(128) DEFAULT NULL COMMENT 'group_id',
`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 COMMENT 'app_name',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description',
`c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage',
`effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述',
`type` varchar(64) DEFAULT NULL COMMENT '配置的类型',
`c_schema` text COMMENT '配置的模式',
`encrypted_data_key` text NOT NULL COMMENT '密钥',
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';
/******************************************/
/* 表名称 = 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(128) 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 COMMENT 'app_name',
`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='增加租户字段';
/******************************************/
/* 表名称 = 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 '租户字段',
`encrypted_data_key` text NOT NULL 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';
/******************************************/
/* 表名称 = 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';
/******************************************/
/* 表名称 = 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 COMMENT 'nid, 自增长标识',
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';
/******************************************/
/* 表名称 = 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容量信息表';
/******************************************/
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(20) unsigned NOT NULL COMMENT 'id',
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识',
`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',
`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',
`op_type` char(10) DEFAULT NULL COMMENT 'operation type',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`encrypted_data_key` text NOT NULL 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='多租户改造';
/******************************************/
/* 表名称 = 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 COMMENT 'username',
`password` varchar(500) NOT NULL COMMENT 'password',
`enabled` boolean NOT NULL COMMENT 'enabled'
);
CREATE TABLE `roles` (
`username` varchar(50) NOT NULL COMMENT 'username',
`role` varchar(50) NOT NULL COMMENT 'role',
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL COMMENT 'role',
`resource` varchar(255) NOT NULL COMMENT 'resource',
`action` varchar(8) NOT NULL COMMENT 'action',
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');
编辑nacos的环境变量custom.env文件
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=127.0.0.1
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
利用Docker部署
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
访问ip:8848/nacos即可跳转到控制台,账号密码均为nacos
服务注册
Nacos服务部署好了,如何让其他服务注册到Nacos呢?
只需引入依赖,配置nacos地址,重启服务即可
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
重启服务,进入控制台,服务管理-服务列表中即可查看,这里多实例部署都会注册到nacos中
服务发现与调用
服务发现需要用到DiscoveryClient,SpringCloud已经自动装配,可以直接注入。
然后修改我们的远程调用
//2.1根据服务名称获取实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if (CollUtils.isEmpty(instances)){
return;
}
//2.2简易负载均衡,从实例列表挑选一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
//2.3利用RestTemplate发起http请求,得到http响应
//instance.getUri()获取服务ip和端口
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
instance.getUri()+"/items?ids={ids}", //请求url
HttpMethod.GET, //请求方式
null, //请求实体
new ParameterizedTypeReference<List<ItemDTO>>() {}, //返回值类型
Map.of("ids", CollUtils.join(itemIds, ",")) //请求参数
);
//2.4解析响应结果...
但是这种方式每次发起调用都要编写一大段臃肿的代码冗余在业务层,对于相同的远程调用代码复用性低,要修改简化一下。
OpenFeign
引入依赖
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
在需要调用服务的启动类上加注解@EnableFeignClients,启用OpenFeign功能
之后需要编写Feign的客户端接口,考虑到多个服务可能会需要相同的服务提供者提供相同的服务,为避免重复编码,可以将其抽取出来。
方案一:抽取到一个新的工程模块中(简单,结构清晰,但是项目耦合度会偏高)
方案二:每一个服务自己抽取出对外提供的api模块(结构复杂,但是耦合度低)
方案一步骤如下:
新建Module,引入依赖,其他需要远程调用的服务引入该Module即可。
编写Feign客户端
@FeignClient("item-service")//声明要调用的服务名称
public interface ItemClient {
@GetMapping("/items")//请求方式和请求路径
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);//请求参数和请求返回值类型
}
调用FeignClient
注入要使用的FeignClient,调用其方法
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
配置扫描包
由于Feign客户端是在其他模块定义的,其他微服务启动类启动时扫描不到Feign客户端所在的包,需启动类添加声明
方式一:声明扫描包
@EnableFeignClients(basePackages = "com.hmall.api.client")
方式二:声明要用到的FeignClient
@EnableFeignClients(clients = {ItemClient.class})
即可成功运行。
日志配置
OpenFeign的日志级别有四级,只有在DEBUG级别时才会输出日志
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
自定义Open Feign的日志级别
定义一个Open Feign的配置类
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
配置配置类生效
局部:在FeignClient中配置,只对当前Client生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
全局:在EnableFeignClients中配置,对所有Client生效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
OpenFeign底层默认是使用JDK的HttpURLConnection发起连接,每次请求都会发起一次连接,不支持连接池并且效率低,整合OKHttp进行优化。
连接池-OKHttp
OpenFeign整合OKHttp步骤如下
引入依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
开启连接池
在服务的yaml配置文件中声明如下
feign:
okhttp:
enabled: true # 开启OKHttp功能
这样,OpenFeign底层的实现就更改为了OKHttp
服务之间的问题解决了,但是前端和后端之间的联系还存在问题,各个服务都独立部署了,前端如何向后端发起请求,前端是无法调用Nacos的,多个服务地址维护起来也十分麻烦,关于用户的认证与校验又应该如何做?
网关
网关(Gateway)是系统的唯一对外的入口,位于客户端和服务器端之间的中间层。它处理非业务功能,提供路由请求、鉴权、监控、缓存、限流等功能。
在微服务中,前端不直接请求服务,而是请求网关,网关再将请求转发到具体的微服务。
网关的使用
网关也是一个微服务,新建一个Module,引入依赖
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
编写网关服务启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
配置网关路由
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: item-service
uri: lb://item-service #lb表示负载均衡,后边表示微服务名称
predicates: #路由断言,判断请求是否符合规则,符合则路由到目标
- Path=/items/**,/search/**
- id: user-service
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: pay-service
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
- id: trade-service
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: cart-service
uri: lb://cart-service
predicates:
- Path=/carts/**
SpringCloudGateway支持的predicates类型很多,官网中有详细解释与用法。
这样,当前端访问网关时就能够正确的将请求转发到对应的微服务中。
网关的登录校验
所有的请求都会先经过网关,所以可以把登录校验放在网关当中,避免将登录校验工作放在所有微服务中的重复编码和密钥泄密的安全问题。
随之而来的也是新的问题:如何在网关转发请求前进行登录校验,并将用户信息由网关传递到微服务?微服务之间的相互调用又如何传递用户信息?
网关过滤器
网关当中有众多过滤器形成了一条过滤器链,其中转发请求是由最后一个过滤器NettyRoutingFilter完成的,只需要定义一个完成登录校验功能的过滤器,并将它的顺序定义在NettyRoutingFilter之前就可以了。
网关中有两种过滤器:
GatewayFilter:路由过滤器,可以指定任意路由
GlobalFilter:全局过滤器,作用于所有路由,不可配置
Gateway内置了许多GatewayFilter,使用简单,只需在yaml文件中配置在需要的路由下即可
spring:
application:
name: gateway
cloud:
nacos:
server-addr: ip:8848
gateway:
routes:
- id: item-service
uri: lb://item-service #lb表示负载均衡,后边表示微服务名称
predicates: #路由断言,判断请求是否符合规则,符合则路由到目标
- Path=/items/**,/search/**
# 添加请求头,只对该路由生效
filters:
- AddRequestHeader=truth,1+1=2
如果要配置全局路由,只需如下即可
spring:
application:
name: gateway
cloud:
nacos:
server-addr: ip:8848
gateway:
# 默认过滤器,对所有的路由都生效
default-filters:
- AddRequestHeader=truth,1+1=2
routes:
- id: item-service
uri: lb://item-service #lb表示负载均衡,后边表示微服务名称
predicates: #路由断言,判断请求是否符合规则,符合则路由到目标
- Path=/items/**,/search/**
自定义过滤器
自定义GatewayFilter:
//定义完后在yaml文件配置才能生效
//类名必须是XXXGatewayFilterFactory,配置文件中 XXX=参数 无参则直接XXX
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
//无法指定过滤器的顺序 除非重写一个类实现GatewayFilter和Ordered,然后new这个类
/*return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("print any filter running");
return chain.filter(exchange);
}
};*/
//方式二 装饰类,指定顺序
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String a = config.getA();
String b = config.getB();
String c = config.getC();
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
System.out.println("print any filter running");
return chain.filter(exchange);
}
},1);
}
//自定义配置属性
@Data
public static class Config{
private String a;
private String b;
private String c;
}
@Override
public List<String> shortcutFieldOrder() {
//定义读参数顺序
return List.of("a","b","c");
}
//将字节码传递给父类来读取yaml文件配置
public PrintAnyGatewayFilterFactory() {
super(Config.class);
}
}
yaml文件中配置如下
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 参数以","隔开,按照shortcutFieldOrder()方法返回的参数顺序依次复制
或是手动指定
spring:
cloud:
gateway:
default-filters:
- name: PrintAny
args: # 手动指定参数名,无需按照参数顺序
a: 1
b: 2
c: 3
自定义GlobalFilter:
@Component
public class MyGlobalFilter implements GlobalFilter , Ordered {
//exchange 上下文
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//模拟登录校验逻辑
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
System.out.println("headers="+headers);
//放行
return chain.filter(exchange);
}
//控制顺序,数字越小优先级越高
@Override
public int getOrder() {
return 0;
}
}
NettyRoutingFilter过滤器的优先级是Int的最大值,优先级最低
登录校验逻辑
网关
对于无需登录验证即可放行的请求,利用AntPathMatcher进行匹配,只需注入,然后匹配时调用match方法即可
#源码
public boolean match(String pattern, String path) {
return this.doMatch(pattern, path, true, (Map)null);
}
pattern代表放行的请求路径,path表示当前请求路径
对于登录校验后的请求,向微服务传递用户信息时,在请求头中添加请求头存入用户信息即可
String userInfo = userId.toString();
ServerWebExchange serverWebExchange = exchange.mutate() //mutate是对下游请求做修改
.request(builder -> builder.header("user-info", userInfo))
.build();
微服务
通过拦截器获取到请求头,存入ThreadLocal后放行
拦截器的实现比较简单,需要注意的是拦截器的配置
@Configuration
//其他微服务默认是扫描不到的,还要在META-INF包下配置才可生效
//spring自动装配条件注解
//微服务有springmvc一定有这个类,网关没有springmvc没有这个类
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
META-INF包下spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
XXX.MvcConfig
这样就实现了网关到微服务之间的信息传递
微服务之间传递
借助Feign中的拦截器接口RequestInterceptor
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
// feign提供的拦截器,远程调用的时候生效
// 注意要使配置类生效 (微服务启动类配置)
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = UserContext.getUser();
if (userId != null) {
requestTemplate.header("user-info", userId.toString());
}
}
};
}
}
启动类配置注解
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
即可实现微服务之间信息传递