SpringCloud实用篇
微服务介绍
需要学习哪些微服务知识?
认识微服务
服务架构
单体架构
分布式架构
微服务
总结
微服务技术对比
企业需求
SpringCloud
- SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。
- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验.
服务拆分及远程调用
服务拆分
远程调用
远程调用方式
订单模块可以像浏览器一样给用户模块发送一个得到用户信息的请求, 用户模块收到请求, 将用户信息响应回给订单模块.
基于RestTemplate发起的http请求实现远程调用http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。
远程调用步骤
注册RestTemplate
因为是订单模块要向用户信息模块发请求, 所以在
order-service的OrderApplication中注册RestTemplate1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
public RestTemplate restTemplate() {
return new RestTemplate();
}
}服务远程调用RestTemplate
修改order-service中的OrderService的
queryOrderById方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2. 发送获得用户信息请求, 并获得用户信息
String url = "http://localhost:8081/user/" + order.getUserId();
//getForObject是发送get请求, postForObject是post
//第一个参数是url, 第二个参数将的到的json转化成什么类型
User user = restTemplate.getForObject(url, User.class);
//3. 将用户信息写入order
order.setUser(user);
// 4.返回订单信息
return order;
}
Eureka注册中心
Eureka的作用
服务调用出现的问题
- 服务消费者该如何获取服务提供者的地址信息?
- 如果有多个服务提供者,消费者该如何选择?
- 消费者如何得知服务提供者的健康状态?
当服务提供者的其中一个端口停止向注册中心心跳续约, 注册中心将会将该端口从服务列表中剔除.
消费者该如何获取服务提供者具体信息?
- 服务提供者启动时向eureka注册自己的信息
- eureka保存这些信息
- 消费者根据服务名称向eureka拉取提供者信息
如果有多个服务提供者,消费者该如何选择?
- 服务消费者利用负载均衡算法,从服务列表中挑选一个
消费者如何感知服务提供者健康状态?
- 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
- eureka会更新记录服务列表信息,心跳不正常会被剔除
- 消费者就可以拉取到最新的信息
搭建EurekaServer
搭建EurekaServer服务步骤如下:
创建maven项目,引入spring-cloud-starter-netflix-eureka-server的依赖
1
2
3
4
5<!--Eureka服务端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>编写启动类,添加
@EnableEurekaServer注解1
2
3
4
5
6
7
//Eureka服务的开关
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}添加application.yml文件,编写下面的配置:
1
2
3
4
5
6
7
8
9server:
port: 10089
spring:
application:
name: eurekaserver
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10089/eureka/
注册client端
将user-service服务注册到EurekaServer步骤如下:
在
user-service项目引入spring-cloud-starter-netflix-eureka-client的依赖1
2
3
4
5<!--Eureka客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>在application.yml文件,编写下面的配置
1
2
3
4
5
6
7spring:
application:
name: userserver //服务名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10089/eureka/
其他服务注册类似, 只需要将yml文件中的服务名称更改即可.
另外,我们可以将user-service多次启动, 模拟多实例部署,但为了避免端口冲突,需要修改端口设置:
Eureka界面
启动所有服务后, 打开http://localhost:10089, 出现以下界面
红框里的就是服务列表.
服务拉取
服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡
修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口
1
2
3//2. 发送获得用户信息请求, 并获得用户信息
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解
1
2
3
4
5
//负载均衡注解
public RestTemplate restTemplate() {
return new RestTemplate();
}
Ribbon负载均衡
负载均衡流程
- 订单模块发起获得用户信息请求.
- 请求被负载均衡拦截器拦截,
RibbonLoadBanlancerClient获取url中的服务id交给DynamicServerListLoadBalancer DynamicServerListLoadBalancer向eureka-server拉取userservice列表信息.- eureka-server将userservice的服务列表返回给
DynamicServerListLoadBalancer. DynamicServerListLoadBalancer将服务列表交给IRule, IRule根据规则(这里是负载均衡)将某个服务返回给RibbonLoadBanlancerClient.RibbonLoadBanlancerClient修改一开始获得url, 然后发起请求.
负载均衡策略
Ribbon的负载均衡规则是一个叫做IRule的接口来定义的,每一个子接口都是一种规则.
默认实现是ZoneAvoidanceRule,根据zone选择服务列表,然后轮询
修改策略
通过定义IRule实现可以修改负载均衡规则, 例如修改成RandomRule规则,有两种方式
代码方式:在
order-service中的OrderApplication类中,定义一个新的IRule1
2
3
4
public IRule randomRule() {
return new RandomRule();
}配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则
1
2
3userservice: //被指定规则的服务名
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
然后访问”http://localhost:8080/order/不同的订单id“ 后查看user-service的控制台日志可以发现确实改变了规则.
对比
- 代码方式: 配置灵活,但修改时需要重新打包发布.
- 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置.
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载
1 | ribbon: |
Nacos注册中心
安装Nacos(Linux)
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高
安装JDK
Nacos依赖于JDK运行,所以Linux上也需要安装JDK才行。
在/usr/local中, 新建一个java目录, 将jdk安装包上传到/usr/local/java中, 然后cd到java目录中解压缩:
1 | tar -xvf jdk-8u144-linux-x64.tar.gz |
随后cd到根目录, 输入vim /etc/profile.d/java.sh配置环境变量:
1 | export JAVA_HOME=/usr/local/java/jdk1.8.0_144 |
设置环境变量:
1 | source /etc/profile |
输入jps, 出现数字表示配置环境变量成功.
安装Nacos
在/usr/local中, 新建一个nacos目录, 将Nacos安装包上传到/usr/local/nacos中, 然后cd到nacos目录中解压缩
1 | tar -xvf nacos-server-1.4.1.tar.gz |
然后删除安装包:
1 | rm -rf nacos-server-1.4.1.tar.gz |
cd到/usr/local/nacos/bin目录中,输入命令启动Nacos:
1 | sh startup.sh -m standalone |
启动日志如下
1 | nacos is starting with standalone |
服务注册到Nacos
在父工程中添加spring-cloud-alilbaba的管理依赖
1
2
3
4
5
6
7
8<!--nacos的管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>注释掉order-service和user-service中原有的eureka依赖
添加nacos的客户端依赖
1
2
3
4
5
6<!-- nacos客户端依赖包 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>修改user-service&order-service中的application.yml文件,注释eureka地址,添加nacos地址
1
2
3
4spring:
cloud:
nacos:
server-addr: LinuxIP地址:8848 # nacos 服务端地址
Nacos服务分级存储模型
服务调用尽可能选择本地集群的服务,跨集群调用延迟较高, 本地集群不可访问时,再去访问其它集群
设置服务集群属性
修改application.yml,添加如下内容
1
2
3
4
5cloud:
nacos:
server-addr: LinuxIp:8848 # nacos 服务端地址
discovery:
cluster-name: HZ # 配置集群名称,也就是机房位置,例如:HZ,杭州在Nacos控制台可以看到集群变化
根据集群负载均衡
在order-service中设置负载均衡的IRule为NacosRule,这个规则优先会寻找与自己同集群的服务
1 | userservice: |
根据权重负载均衡
实际部署中会出现这样的场景:服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求.
Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高, 权重设置为0则完全不会被访问
环境隔离 - namespace
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离
在Nacos控制台可以创建namespace,用来隔离不同环境
然后填写一个新的命名空间信息
保存后会在控制台看到这个命名空间的id
修改order-service的application.yml,添加namespace
1
2
3
4
5
6
7spring:
cloud:
nacos:
server-addr: LinuxIp:8848 # nacos 服务端地址
discovery:
cluster-name: HZ # 配置集群名称,也就是机房位置,例如:HZ,杭州
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID, 切记不是名字dev重启order-service后,再来查看控制台
此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错
临时实例和非临时实例
服务注册到Nacos时,可以选择注册为临时或非临时实例,临时实例宕机时,会从nacos的服务列表中剔除,而非临时实例则不会.
通过下面的配置来设置
1 | spring: |
Nacos与eureka比较
Nacos与eureka的共同点
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康检测
Nacos与Eureka的区别
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
Nacos配置管理
统一配置管理
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
配置获取步骤
项目启动后先读取bootstrap.yml文件, 根据bootstrap.yml文件中的配置得到需要在Nacos中读取的配置文件名称, 读取到Nacos中的配置文件后再去读取本地配置文件application.yml文件, 接着创建spring容器, 加载bean.
Nacos配置设置步骤
注意: 不是所有的配置都适合放到配置中心,维护起来比较麻烦建议将一些关键参数,需要运行时调整的参数放到nacos配置中心,一般都是自定义配置.
在Nacos中添加配置信息
在弹出表单中填写配置信息(注意缩进!!!)
在user-service服务中,引入nacos-config的客户端依赖
1
2
3
4
5
6<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>然后,在user-service中添加一个bootstrap.yaml文件(这里的配置需要根据Nacos配置文件名填写),内容如下
1
2
3
4
5
6
7
8
9
10spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: LinuxIP:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。本例中,就是去读取
userservice-dev.yaml删除application.yml中和bootstrap.yml重复的配置, 如服务名称、nacos地址等
在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//注意要加这个注释
public class UserController {
private String dateformat;
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
配置自动刷新
Nacos中的配置文件变更后,微服务无需重启就可以感知。
实现自动刷新有两种方法(推荐使用第二种):
- 方法一: 在@Value注入的变量所在类上添加注解@RefreshScope
方式二:新建一个PatternPorperties类, 在类中定义配置文件中的属性为成员变量, 并在类上使用@ConfigurationProperties注解. 在需要调用Nacos配置文件的属性时可以使用@Autowired注释来获得PatternPorperties类对象, 使用get方法获得对象.
新建PatternProperties类
1
2
3
4
5
6
public class PatternProperties {
private String dateformat;
}在UserController类中自动装配获得类对象
1
2
3
4
5
6
7
8
9public class UserController {
private PatternProperties properties;
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}
}
多环境配置共享
微服务启动时会从nacos读取多个配置文件
- [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml
- [spring.application.name].yaml,例如:userservice.yaml
无论profile如何变化,[spring.application.name].yaml这个文件一定会加载,因此多环境共享配置可以写入这个文件中
添加多环境配置步骤
在Nacos页面新建一个userservice.yml配置文件, 加入属性
更新PatternProperties类的成员变量
1
2
3
4
5
6
7
public class PatternProperties {
private String dateformat;
private String envShareValue;
}在UserController中新建接口
1
2
3
4
5
6
7
private PatternProperties properties;
public PatternProperties properties() {
return properties;
}先启动一个环境是dev的userservice, 然后对另一个未启动的userserver按下 “shift+F4”, 在最下面的”Active profiles”中填入”test”修改环境, 然后运行测试.
最后发现共享环境对两个userservice都生效, dev环境的配置对test环境的userservice不生效.
多种配置的优先级
[服务名]-[环境].yaml >[服务名].yaml > 本地配置
共享配置文件
不同微服务之间可以共享配置文件,通过下面的两种方式来指定
方式一
1
2
3
4
5
6
7
8
9
10
11
12spring:
application:
name: userservice # 服务名称
profiles:
active: dev # 环境,
cloud:
nacos:
server-addr: LinuxIP:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 多微服务间共享的配置列
- dataId: common.yaml # 要共享的配置文件id方式二
1
2
3
4
5
6
7
8
9
10
11
12spring:
application:
name: userservice # 服务名称
profiles:
active: dev # 环境,
cloud:
nacos:
server-addr: LinuxIP:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
extends-configs: # 多微服务间共享的配置列表
- dataId: extend.yaml # 要共享的配置文件id
Nacos集群搭建
Nacos生产环境下一定要部署为集群状态, 搭建集群的基本步骤:
- 搭建数据库,初始化数据库表结构
- 下载nacos安装包
- 配置nacos
- 启动nacos集群
- nginx反向代理
初始化数据库
Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。
官方推荐的最佳实践是使用带有主从的高可用数据库集群, 这里我们以单点的数据库为例来讲解。
首先新建一个数据库,命名为nacos,而后导入下面的SQL:
1 | CREATE TABLE `config_info` ( |
配置Nacos
进入到nacos文件中, 其中有以下两个重要文件
- bin:启动脚本
- conf:配置文件
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:
在#example下面修改添加自己的Nacos地址和端口:
1 | #example |
然后修改application.properties文件,添加数据库配置
1 | spring.datasource.platform=mysql |
启动
将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3
然后分别修改三个文件夹中的application.properties,
nacos1:
1 | server.port=8845 |
nacos2:
1 | server.port=8846 |
nacos3:
1 | server.port=8847 |
然后在三个Nacos文件的并目录下分别启动三个nacos节点:
1 | startup.cmd |
nginx反向代理
修改conf/nginx.conf文件,配置如下:
1 | upstream nacos-cluster { |
而后在浏览器访问:http://localhost/nacos即可。
代码中application.yml文件配置如下:
1 | spring: |
优化
实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.
Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离
http客户端Feign
Feign的介绍
RestTemplate方式调用存在的问题
先来看我们以前利用RestTemplate发起远程调用的代码
1 | String url = "http://userservice/user/" + order.getUserId(); |
存在下面的问题:
- 代码可读性差,编程体验不统一
- 当遇到参数复杂的URL时难以维护
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
定义和使用Feign客户端
使用Feign的步骤如下
在order-service服务的pom文件中引入feign的依赖
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>在order-service的启动类添加注解开启Feign的功能
在order-service中新建一个client包, 在包内新建一个接口,内容如下
1
2
3
4
5
6
7
8
9
10
11
12package cn.itcast.order.client;
import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
//注意该服务名称是在Nacos中注册的服务名
public interface UserClient {
User findById( Long id);
}这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:
- 服务名称:
@FeignClient("userservice"), 注意该服务名称是在Nacos中注册的服务名 - 请求方式:
@GetMapping - 请求路径:
/user/{id} - 请求参数:
@PathVariable("id") Long id - 返回值类型:
User
这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。
- 服务名称:
在OrderService中调用UserClient接口来完成远程调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OrderService {
private OrderMapper orderMapper;
private UserCilent userCilent;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
//2. 使用Feign远程调用, 获得用户信息
User user = userCilent.findById(order.getUserId());
//3. 将用户信息写入order
order.setUser(user);
// 4.返回
return order;
}
}
自定义Feign的配置
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下
| 类型 | 作用 | 说明 |
|---|---|---|
| feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
| feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
| feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
| feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
| feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
下面以日志为例来演示如何自定义配置。
配置Feign日志
配置Feign日志有两种方式
方式一: 配置文件方式
①全局生效
1
2
3
4
5feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别②针对单个服务
1
2
3
4
5feign:
client:
config:
userservice: # 针对某个微服务名, 服务名是在Nacos中注册的服务名
loggerLevel: FULL # 日志级别方式二:java代码方式
在orderservice中新建一个config包, 在包中新建一个FeignConfiguration类
1
2
3
4
5
6
7
8
9
10
11package cn.itcast.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class FeignConfiguration {
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; //日志的级别
}
}而后如果是全局配置,则把它放到orderservice启动类的@EnableFeignClients这个注解中
1
如果是局部配置,则把它放到接口的@FeignClient注解中
1
Feign的性能优化
Feign底层的客户端实现:
- URLConnection:默认实现,不支持连接池
- Apache HttpClient :支持连接池
- OKHttp:支持连接池
因此优化Feign的性能主要包括:
- 使用连接池代替默认的URLConnection
- 日志级别,最好用basic或none
连接池配置
这里以Feign添加HttpClient的支持为例
在order-service的pom文件中引入Apache的HttpClient依赖
1
2
3
4
5<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>在order-service的application.yml中添加配置
1
2
3
4
5
6
7
8
9feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
Feign的最佳实践
仔细观察可以发现,Feign的客户端与服务提供者的controller代码非常相似.
有以下两种方法简化重复的代码编写.
方式一(继承)
给消费者的FeignClient和提供者的controller定义统一的父接口作为标准, 一样的代码可以通过继承来共享.
实现步骤如下.
新建一个Module, 命名为feign-api
定义一个API接口,定义返回类型是泛型的方法,并基于SpringMVC注解做声明。
1
2
3
4
5
6
7
8
9
10
11package cn.itcast.feign.api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
public interface UserAPI<T> {
T findById( Long id);
}为了避免类型不统一的情况, **orderservice(消费者)应删除userservice(提供者)**的
User(相关pojo)类, 并在pom文件中引入userservice的依赖, 将order类中的User类更改成userservice中的User类.在userservice的pom文件中引入
feign-api依赖, 然后Controller类直接实现UserAPI<User>接口(泛型一定要指定), 只需要加@RestController注解. 除了重写的方法不指定请求的类型, 非重写的方法仍要写请求类型.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserController implements UserAPI<User> {
private UserService userService;
public User findById( Long id) {
return userService.queryById(id);
}
public PatternProperties properties() {
return properties;
}
}orderservice的
UserCilent接口直接继承UserAPI<User>, 并加上@FeignClient(value = "userservice")注解, 注意泛型一定要指定.1
2
3
4
5
6
7
8
9package cn.itcast.order.clients;
import cn.itcast.feign.api.UserAPI;
import cn.itcast.user.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
public interface UserCilent extends UserAPI<User> {
}orderservice调用
UserCilent接口.1
2//2. 使用Feign远程调用, 获得用户信息
User user = userCilent.findById(order.getUserId());
方式一因为课程里并没有讲怎么具体实现, 以上步骤是由写者自行实现的, 所以可能有些处理不好的地方, 但确实是可以运行的.
优点:
- 简单
- 实现了代码共享
缺点:
服务提供方、服务消费方紧耦合
参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解
方式二(抽取)
将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用
抽取
首先创建一个module,命名为feign-api, 在feign-api中然后引入feign的starter依赖
1 | <dependency> |
然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
在order-service中使用feign-api
首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。
在order-service的pom文件中中引入feign-api的依赖:
1 | <dependency> |
修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包
解决扫描包问题
此时因为UserClient现在在cn.itcast.feign.clients包下,而order-service的@EnableFeignClients注解是在cn.itcast.order包下,不在同一个包,无法扫描到UserClient。解决该问题有如下两种方法, 推荐使用第二种.
方式一:指定Feign应该扫描的包(
@EnableFeignClients(basePackages = "cn.itcast.feign.clients"))1
2
3
4
5
6
7
8
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}方式二:指定需要加载的Client接口(
@EnableFeignClients(clients = {UserCilent.class})) (推荐)1
2
3
4
5
6
7
8
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
Gateway服务网关
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
网关介绍
网关功能
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
网关技术
在SpringCloud中网关的实现包括两种:
- gateway
- zuul
Zuul是基于Servlet的实现,属于阻塞式编程。
而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
搭建网关服务
搭建网关的基本路由步骤如下
新建一个名为gateway的Module,引入如下依赖
1
2
3
4
5
6
7
8
9
10<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>编写启动类
1
2
3
4
5
6
7
8
9
10
11
12package cn.itcast.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}创建application.yml文件,内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: LinuxIP:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
路由配置包括:
路由id:路由的唯一标示
路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
路由断言(predicates):判断路由的规则,
路由过滤器(filters):对请求或响应做处理
我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址。
本例中,我们将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
启动网关启动类,访问http://localhost:10010/user/1时,符合`/user/**`规则,请求转发到uri:http://userservice/user/1,得到了结果
断言工厂
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件.
例如Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来
处理的,像这样的断言工厂在SpringCloudGateway还有十几个:
| 名称 | 说明 | 示例 |
|---|---|---|
| After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
| Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
| Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
| Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
| Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
| Host | 请求必须是访问某个host(域名) | - Host=** .somehost.org,**.anotherhost.org |
| Method | 请求方式必须是指定方式 | - Method=GET,POST |
| Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
| Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
| RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
| Weight | 权重处理 |
我们只需要掌握Path这种路由工程就可以了。
例如: 在断言中加入After
1 | gateway: |
我们重启网关服务, 发现http://localhost:10010/user/1已经访问不了了, 因为我们在断言中加入After2031, 而现在是2022年.
过滤器工厂
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
路由过滤器的种类
Spring提供了31种不同的路由过滤器工厂。例如:
| 名称 | 说明 |
|---|---|
| AddRequestHeader | 给当前请求添加一个请求头 |
| RemoveRequestHeader | 移除请求中的一个请求头 |
| AddResponseHeader | 给响应结果中添加一个响应头 |
| RemoveResponseHeader | 从响应结果中移除有一个响应头 |
| RequestRateLimiter | 限制请求的流量 |
请求头过滤器
下面我们以AddRequestHeader 为例来讲解。
需求:给所有进入userservice的请求添加一个请求头:Truth=itcast is freaking awesome!
只需要修改gateway服务的application.yml文件,添加路由过滤即可:
1 | spring: |
然后在UserController中修改方法获取下请求头, 注意UserAPI也要改方法
1 |
|
当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。
默认过滤器
如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:
1 | spring: |
总结
过滤器的作用是什么?
① 对路由的请求或响应做加工处理,比如添加请求头
② 配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
① 对所有路由都生效的过滤器
全局过滤器
网关提供的31种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现, 此时就可以使用全局过滤器。
全局过滤器作用
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口。
1 | public interface GlobalFilter { |
在filter中编写自定义逻辑,可以实现下列功能:
- 登录状态判断
- 权限校验
- 请求限流等
自定义全局过滤器
定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
参数中是否有authorization,
authorization参数值是否为admin
如果同时满足则放行,否则拦截
1 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
设置自定义拦截器优先级
设置自定义拦截器的优先级有两种方法
在自定义类上加上
@Order()注解, 注解括号内填[-2147483647, 2147483647]的整数, 数字越小优先级越高1
2
3
public class AuthorizeFilter implements GlobalFilter {... }自定义类再实现一个名为
Ordered的接口, 实现接口内的getOrder方法, 返回一个int类型, 数字越小优先级越高1
2
3
4
5
6
7
8
public class AuthorizeFilter implements GlobalFilter, Ordered {
public int getOrder() {
return -1;
}
...
}
过滤器执行顺序
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器.
排序的规则是什么呢?
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
- 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
详细内容,可以查看源码:
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。
org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链
跨域问题
什么是跨域问题
跨域:域名不一致就是跨域,主要包括:
域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com
域名相同,端口不同:localhost:8080和localhost8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决方案:CORS,这个以前应该学习过,这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html
解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置
1 | spring: |


















































