SpringCloud
概述
- Spring Cloud是一系列框架的有序集合。
- Spring Cloud的本质是在 Spring Boot 的基础上,增加了一堆微服务相关的规范,并对应用上下文 (Application Context )进行了功能增强。
- 它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
优点 | 缺点 |
---|---|
通过服务的原子化拆分,以及微服务的独立打包、部署和升级,小团队的交付周期将缩短,运维成本也将大幅度下降 | 微服务过多,服务治理成本高,不利于系统维护。 |
微服务遵循单一原则。微服务之间采用Restful等轻量协议传输。 | 分布式系统开发的技术成本高(容错、分布式事务等)。 |
注意
微服务之间传递参数时 @RequestBody @PathVariable 不能省略!
spring:
application:
name: eureka #微服务项目的名字,会在注册中心中出现
RestTempalte
代码:
消费者代码:
package com.sms.controller;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:44
*/
@RestController
@RequestMapping("/conStudent")
public class StudentController {
@Autowired
RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
/**
* eureka 获取实例地址方法
*
* @return
*/
public String serviceUri() {
// provider-student 服务提供者
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("provider-student");
if (serviceInstances != null && serviceInstances.size() > 0) {
return serviceInstances.get(0).getUri() + "/student/";
}
return null;
}
@GetMapping("/{id}")
public RespEntity getStuInfo(@PathVariable("id") Integer id) {
System.out.println("获取一个学生信息");
//远程调用
return restTemplate.getForObject(serviceUri() + id,
RespEntity.class);
}
@GetMapping("/")
public RespEntity getAllStudent() {
System.out.println("获取所有学生信息");
//远程调用
return restTemplate.getForObject(serviceUri(),
RespEntity.class);
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println("添加学生");
System.out.println(student);
return restTemplate.postForObject(serviceUri(), student, RespEntity.class);
}
}
提供者代码:
package com.sms.controller;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:31
*/
@RestController
@RequestMapping("/student")
public class StudentController {
@Value("${server.port}")
private Integer port;
@GetMapping("/{id}")
public RespEntity getStudent(@PathVariable("id") Integer id) {
if (id == 1) {
return new RespEntity(1, "ok"+port, new Student(1, "test1", 12));
}
return new RespEntity(2, "ok"+port, new Student(2, "test2", 11));
}
@GetMapping("/")
public RespEntity getAllStudent() {
return new RespEntity(2, "ok"+port, Arrays.asList(
new Student(1, "test1", 12),
new Student(2, "test2", 11),
new Student(3, "test3", 13)
));
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println(student);
return new RespEntity(200, "ok"+port, null);
}
}
Eureka
概述:
Eureka是Netflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflix中, 实现SpringCloud的服务发现功能。
微服务注册中心
原理:注册中心可以说是微服务架构中的通讯录,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到注册中心,当服务需要调用其它服务时,就到注册中心找到服务的地址,进行调用。
常见的注册中心:
组件名 | 语言 | CAP | 一致性算法 | 对外暴露接口 |
---|---|---|---|---|
Eureka | Java | AP | 无 | HTTP |
Consul | Go | CP | Raft | HTTP/DNS |
Zookeeper | Java | CP | Paxos | 客户端 |
Nacos | Java | AP | Raft | HTTP |
CAP:三进二
C:一致性;
A:可用性;
P:分区容错性
在微服务下,分区容错性是必须实现,其一致性和可用性二选一,为了优化用户体验,一般会优先实现可用性,保证服务是可用状态。
基本架构:
Eureka Server :提供服务注册和发现 ;
Eureka Client:
Service Provider :服务提供方(将自身服务注册到Eureka ,从而使服务消费方能够找到 )
Service Consumer : 服务消费方 (从Eureka获取注册服务列表,从而能够消费服务)
- Eureka Server提供服务发现的能力,各个微服务启动时,会通过Eureka Client向Eureka Server 进行注册自己的信息(例如网络信息),Eureka Server会存储该服务的信息;
- 微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息。如果Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服 务节点(默认90秒);
- 每个Eureka Server同时也是Eureka Client ,多个Eureka Server之间通过复制的方式完成服务注册表的同步;
- Eureka Client会缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者
自我保护:
- 微服务第一次注册成功之后,每30秒会发送一次心跳将服务的实例信息注册到注册中心。通知 Eureka Server该实例仍然存在。
- 如果超过90秒没有发送更新,则服务器将从注册信息中将此服务移除。
- Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85% ,如果出现低于的情况,Eureka Server会 将当前的实例注册信息保护起来,同时提示这个警告。
- 保护模式主要用于一组客户端和Eureka Server 之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)
关闭自我保护功能: eureka.service.enableSelfPreservation=false
服务剔除:
lease-renewal-interval-in-seconds: 服务续约(renew)的间隔, 默认为30秒
lease-expiration-duration-in-seconds: 服务失效时间,默认值90秒
也就是说,默认情况下每隔30秒服务会向注册中心发送一次心跳, 证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会定时(eureka.server.eviction-interval-timer.in-ms设定的时间,默认60秒) 从服务列表中移除,这两个值在生产环境不要修改,默认即可。
服务续约:
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求) 。告诉EurekaServer: “我还活着"。称为服务的续约(renew) ;
服务失效:
lease-expiration-duration-in-seconds: 服务失效时间,默认值90秒
代码
pom:
<!--eureka 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
yml:
服务端:
server:
port: 6061
spring:
application:
name: eureka #微服务项目的名字,会在注册中心中出现
eureka:
instance:
hostname: localhost #实例名
client:
# registerWithEureka: false # 是否将自己挂到注册中心上,单节点可以false,集群true
# fetchRegistry: false # 是否要去注册获取服务列表
registerWithEureka: true # 是否将自己挂到注册中心上,单节点可以false,集群true
fetchRegistry: true # 是否要去注册获取服务列表
serviceUrl:
defaultZone: http://localhost:6061/eureka/ #Eureka服务地址
# defaultZone: http://localhost:6061/eureka/,http://localhost:6062/eureka/ #Eureka服务地址
server:
enable-self-preservation: false #禁用自我保护
#private long evictionIntervalTimerInMs = 60000L;
#启用删除的默认时间:默认60s启动一次剔除服务,将无效的服务从服务列表删除
eviction-interval-timer-in-ms: 2000
客户端:
server:
# 端口
port: 8081
spring:
application:
name: provider-student
eureka:
client:
registerWithEureka: true # 是否将自己挂到注册中心上,单节点可以false,集群true
fetchRegistry: true # 是否要去注册获取服务列表
serviceUrl:
# defaultZone: http://localhost:6061/eureka/,http://localhost:6062/eureka/ #Eureka服务地址
defaultZone: http://localhost:6061/eureka/ #Eureka服务地址
instance:
#服务续约时间:客户端每隔30秒发送一个心跳包
lease-renewal-interval-in-seconds: 1
#失效时间: 客户端超过90秒没有发送心跳包,服务器就可以认为当前服务下线
lease-expiration-duration-in-seconds: 2
服务提供者:
package com.sms.controller;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:31
*/
@RestController
@RequestMapping("/student")
public class StudentController {
@Value("${server.port}")
private Integer port;
@GetMapping("/{id}")
public RespEntity getStudent(@PathVariable("id") Integer id) {
if (id == 1) {
return new RespEntity(1, "ok"+port, new Student(1, "test1", 12));
}
return new RespEntity(2, "ok"+port, new Student(2, "test2", 11));
}
@GetMapping("/")
public RespEntity getAllStudent() {
return new RespEntity(2, "ok"+port, Arrays.asList(
new Student(1, "test1", 12),
new Student(2, "test2", 11),
new Student(3, "test3", 13)
));
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println(student);
return new RespEntity(200, "ok"+port, null);
}
}
服务消费者:
package com.sms.controller;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:44
*/
@RestController
@RequestMapping("/conStudent")
public class StudentController {
@Autowired
RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
/**
* eureka 获取实例地址方法
*
* @return
*/
public String serviceUri() {
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("provider-student");
if (serviceInstances != null && serviceInstances.size() > 0) {
return serviceInstances.get(0).getUri() + "/student/";
}
return null;
}
@GetMapping("/{id}")
public RespEntity getStuInfo(@PathVariable("id") Integer id) {
System.out.println("获取一个学生信息");
//远程调用
return restTemplate.getForObject(serviceUri() + id,
RespEntity.class);
}
@GetMapping("/")
public RespEntity getAllStudent() {
System.out.println("获取所有学生信息");
//远程调用
return restTemplate.getForObject(serviceUri(),
RespEntity.class);
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println("添加学生");
System.out.println(student);
return restTemplate.postForObject(serviceUri(), student, RespEntity.class);
}
}
Ribbon
概述:
Ribbon是 Netflix发布的一个负载均衡器,有助于控制 HTTP 和 TCP客户端行为。
在 SpringCloud 中,Eureka一般配合Ribbon进行使用,Ribbon提供了客户端负载均衡的功能,Ribbon利用从Eureka中读取到的服务信息,在调用服务节点提供的服务时,会合理的进行负载。
在SpringCloud中可以将注册中心和Ribbon配合使用,Ribbon自动的从注册中心中获取服务提供者的 列表信息,并基于内置的负载均衡算法,请求服务
主要作用:
服务调用 | 负载均衡 |
---|---|
基于Ribbon实现服务调用, 是通过拉取到的所有服务列表组成(服务名-请求路径的)映射关系。借助 RestTemplate 最终进行调用 | 当有多个服务提供者时,Ribbon可以根据负载均衡的算法自动的选择需要调用的服务地址 |
负载均衡:
两种方式:
服务端 | 客户端 |
---|---|
先发送请求到负载均衡服务器或者软件,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配 | 客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配 |
负载均衡策略:
com.netflix.loadbalancer.IRule | 顶级接口 |
---|---|
com.netflix.loadbalancer.RoundRobinRule | 以轮询的方式进行负载均衡。 |
com.netflix.loadbalancer.RandomRule | 随机策略 |
com.netflix.loadbalancer.RetryRule | 重试策略。 |
com.netflix.loadbalancer.WeightedResponseTimeRule | 权重策略。会计算每个服务的权 重,越高的被调用的可能性越大。 |
com.netflix.loadbalancer.BestAvailableRule | 最佳策略。遍历所有的服务实例,过滤掉 故障实例,并返回请求数最小的实例返回。 |
com.netflix.loadbalancer.AvailabilityFilteringRule | 可用过滤策略。过滤掉故障和请 求 |
代码:
@Configuration
public class RestTemplateConfiguration {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
@RequestMapping("/consumerstudent")
public class ConsumerStudent {
@Autowired
RestTemplate restTemplate;
// 直接使用服务名(provider-student)服务提供者
public String serviceUrl() {
return "http://provider-student/student/";
}
@GetMapping("/{id}")
public RespEntity getStuInfo(@PathVariable("id") Integer id) {
System.out.println("获取一个学生信息");
//远程调用
return restTemplate.getForObject(serviceUri() + id,
RespEntity.class);
}
}
OpenFeign
概述:
- Feign是Netflix开发的声明式,模板化的HTTP客户端。
- Feign可帮助我们更加便捷,优雅的调用HTTP API。
- 在SpringCloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。
- SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解,并整合了Ribbon和Eureka ,从而让Feign的使用更加方便。
负载均衡:
Feign中本身已经集成了Ribbon依赖和自动配置,因此我们不需要额外引入依赖,也不需要再注册RestTemplate 对象
超时控制:
默认Feign客户端只等待1秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了 ,直接返回报错。
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
可以在ymI文件中开启配置
#微服务调用超时配置
feign:
client:
config:
default: #default指定对所有的微服务有效,可以指定具体的微服务名
connectTimeout: 5000
readTimeout: 5000
日志打印:
配置
#微服务调用超时配置
feign:
client:
config:
default: #default指定对所有的微服务有效,可以指定具体的微服务名
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full #OpenFeign输出日志信息
#开启spring日志功能,级别:debug
logging:
level:
com.woniuxy.consumer.student.feignclient.StudentFeignClient: debug
代码:
客户端消费者:
pom
<!--Feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
定义声明式接口:
package com.sms.FeignClient;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-27 11:53
*/
@FeignClient("provider-student")
public interface IStudentFeignClient {
@GetMapping("/student/{id}")
RespEntity getStudent(@PathVariable("id") Integer id);
@GetMapping("/student/")
RespEntity getAllStudent();
@PostMapping("/student/")
RespEntity addStudent(@RequestBody Student student);
}
调用接口
package com.sms.controller;
import com.sms.FeignClient.IStudentFeignClient;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:44
*/
@RestController
@RequestMapping("/feignstudent")
public class StudentFeignController {
@Autowired
IStudentFeignClient iStudentFeignClient;
@GetMapping("/{id}")
public RespEntity getStuInfo(@PathVariable("id") Integer id) {
System.out.println("获取一个学生信息");
return iStudentFeignClient.getStudent(id);
}
@GetMapping("/")
public RespEntity getAllStudent() {
System.out.println("获取所有学生信息");
return iStudentFeignClient.getAllStudent();
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println("添加学生");
System.out.println(student);
return iStudentFeignClient.addStudent(student);
}
}
客户端提供者:
package com.sms.controller;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:31
*/
@RestController
@RequestMapping("/student")
public class StudentController {
@Value("${server.port}")
private Integer port;
@GetMapping("/{id}")
public RespEntity getStudent(@PathVariable("id") Integer id) {
if (id == 1) {
return new RespEntity(1, "ok"+port, new Student(1, "test1", 12));
}
return new RespEntity(2, "ok"+port, new Student(2, "test2", 11));
}
@GetMapping("/")
public RespEntity getAllStudent() {
return new RespEntity(2, "ok"+port, Arrays.asList(
new Student(1, "test1", 12),
new Student(2, "test2", 11),
new Student(3, "test3", 13)
));
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println(student);
return new RespEntity(200, "ok"+port, null);
}
}
Hystrix
概述:
Hystrix是一个用于分布式系统的延迟和容错的开源库。
在分布式系统里,许多依赖不可避免的调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整个服务失败,避免级联故障,以提高分布式系统的弹性。
解决问题:服务雪崩
在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者"的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
服务正常时:
其中一个系统出现问题时:
高流情况下:
主要功能:
服务降级 | 服务熔断 | 服务限流 |
---|---|---|
服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback;哪些情况会出发降级:程序运行异常、超时、服务熔断触发服务降级、线程池/信号量打满也会导致服务降级 | 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示,就是保险丝:服务的降级->进而熔断->恢复调用链路 | 秒杀高并发等操作,严禁一窝蜂的过来拥挤, 大家排队,一秒钟N个,有序进行 |
熔断是什么?
熔断机制是应对雪崩效应的一种微服务链路保护机制。
当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,恢复调用链路。
熔断器三种状态?
关闭(Closed):关闭状态(断路器关闭),所有请求都正常访问。
代理类维护了最近调用失败的次数,如果某次调用失败,则使失败次数加1。如果最近失败次数超过了在给定时间内允许失败的阈值,则代理类切换到断开(Open)状态。此时代理开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误。
打开(Open):打开状态(断路器打开),所有请求都会被降级。
Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全关闭。默认失败比例的阈值是50%,请求次数最少不低于20次。
半开(Half Open):半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。
随后断路器会自动进入半开状态。此时会释放1次请求通过,若这个请求是健康的,则会关闭断路器,否则
继续保持打开,再次进行5秒休眠计时。
涉及到断路器的三个重要参数:
-
快照时间窗:
断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
-
请求总数阀值:
在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
-
错误百分比阀值:
当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
代码:
pom
<!--熔断器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
yml
#启用熔断器
feign:
hystrix:
enabled: true
启动类
@EnableCircuitBreaker
@EnableFeignClients
@EnableEurekaClient
@SpringBootApplication
public class ConsumerStudentApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerStudentApplication.class, args);
}
}
定义降级方法
@HystrixCommand(fallbackMethod = "timeoutfallback")
@GetMapping("/timeout")
public ResponseResult timeout() {
return studentFeignClient.timeout();
}
//定义一个降级方法,和对应的方法的方法签名一致(方法名不同,其它都一样)
// 上面方法出问题时走这个方法
public ResponseResult timeoutfallback() {
return new ResponseResult(500, "ttimeout---fallback", null);
}
降级方法统一配置【局限性较大,不推荐使用】
@DefaultProperties(defaultFallback = "xxxxfallback")
@RestController
@RequestMapping("/consumerstudent")
public class ConsumerStudent {
@HystrixCommand
@GetMapping("/timeout")
public ResponseResult timeout() {
return studentFeignClient.timeout();
}
@HystrixCommand
@GetMapping("/timeok")
public ResponseResult timeok() {
return studentFeignClient.timeok();
}
public ResponseResult xxxxfallback() {
return studentFeignClient.timeout();
}
}
整合OpenFeign
OpenFeign接口指定对应的降级处理类
package com.woniuxy.consumer.student.feignclient;
import com.woniuxy.common.entity.ResponseResult;
import com.woniuxy.common.entity.Student;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author author
* @create 2022-09-2022/9/27 11:21
*/
@FeignClient( value = "provider-student",fallback = StudentFeignClientFallback.class)
//@RequestMapping("/student")
public interface StudentFeignClient {
@GetMapping("/student/{sid}")
public ResponseResult<Student> getStudentBySid(@PathVariable("sid") int sid);
@GetMapping("/student/")
public ResponseResult<List<Student>> getAllStudents();
@PostMapping("/student/")
public ResponseResult addStudent(Student student);
@PostMapping("/student/{pageIndex}/{pageSize}")
public ResponseResult getStudentsPager(
@PathVariable("pageIndex") int pageIndex,
@PathVariable("pageSize") int pageSize,
@RequestBody Student student);
@GetMapping("/student/timeout")
public ResponseResult timeout();
@GetMapping("/student/timeok")
public ResponseResult timeok();
}
OpenFeign接口的实现类,定义降级业务逻辑
package com.woniuxy.consumer.student.feignclient;
import com.woniuxy.common.entity.ResponseResult;
import com.woniuxy.common.entity.Student;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author author
* @create 2022-09-2022/9/27 16:32
*/
@Component
public class StudentFeignClientFallback implements StudentFeignClient {
@Override
public ResponseResult<Student> getStudentBySid(int sid) {
return new ResponseResult(500, "getStudentBySid---fallback", null);
}
@Override
public ResponseResult<List<Student>> getAllStudents() {
return new ResponseResult(500, "getAllStudents---fallback", null);
}
@Override
public ResponseResult addStudent(Student student) {
return new ResponseResult(500, "addStudent---fallback", null);
}
@Override
public ResponseResult getStudentsPager(int pageIndex, int pageSize, Student student) {
return new ResponseResult(500, "getStudentsPager---fallback", null);
}
@Override
public ResponseResult timeout() {
return new ResponseResult(500, "timeout---fallback", null);
}
@Override
public ResponseResult timeok() {
return new ResponseResult(500, "timeok---fallback", null);
}
}
Zuul
概述:
网关:
作为微服务的统一入口,可以做身份校验
代码:
pom:
<!--eureka 客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
启动类注解:
@EnableZuulProxy
@SpringBootApplication
public class ZullApplication {
public static void main(String[] args) {
SpringApplication.run(ZullApplication.class, args);
}
}
yml:
server:
port: 9091
spring:
application:
name: zuul
#zuul:
# routes:
# provider-student: #路由id,自定义路由名,可以任意,一般使用微服务名
# path: /student/** #请求路径
# url: http://localhost:8081/ #网关转发的真实请求路径
# consumer-student: #路由id,自定义路由名,可以任意,一般使用微服务名
# path: /consumerstudent/** #请求路径
# url: http://localhost:7071/ #网关转发的真实请求路径
# 面向服务的路由配置
#zuul:
# routes:
# provider-student: #路由id,自定义路由名,可以任意,一般使用微服务名
# path: /student/** #请求路径
# serviceId: provider-student #通过服务名获取服务地址
# consumer-student: #路由id,自定义路由名,可以任意,一般使用微服务名
# path: /consumerstudent/** #请求路径
# serviceId: consumer-student #通过服务名获取服务地址[需要开启eureka注册中心]
#路由id和服务id一致,可以简写 访问时使用服务名直接访问 http://localhost:9091/provider-student/student/1
zuul:
routes:
provider-student: /student/**
consumer-student: /conStudent/**
eureka:
client:
registerWithEureka: true # 是否将自己挂到注册中心上,单节点可以false,集群true
fetchRegistry: true # 是否要去注册获取服务列表
serviceUrl:
# defaultZone: http://localhost:6061/eureka/,http://localhost:6062/eureka/ #Eureka服务地址
defaultZone: http://localhost:6061/eureka/ #Eureka服务地址
网关过滤:
package com.sms.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.sms.entity.RespEntity;
import io.micrometer.core.instrument.util.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-29 14:12
*/
@Component
public class LoginFilter extends ZuulFilter {
/**
* 过滤器类型:
* 拦截时机:pre post error routing
*
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 多个过滤器的执行顺序:越小优先级越高
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* true:执行run();
* false:放行;
*
* @return
*/
@Override
public boolean shouldFilter() {
System.out.println("shouldFilter--进入");
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String user = request.getParameter("user");
if (!StringUtils.isEmpty(user)) {
System.out.println("shouldFilter--放行");
return false;
}
System.out.println("shouldFilter--run");
return true;
}
@Override
public Object run() throws ZuulException {
System.out.println("run--进入");
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String token = request.getParameter("token");
if (!StringUtils.isEmpty(token)) {
System.out.println("run--放行");
return null;
}
System.out.println("run--拦截");
currentContext.setSendZuulResponse(false); // 拦截
// currentContext.setResponseStatusCode(500); // 返回状态码
try {
currentContext.getResponse().setContentType("text/html;charset=utf-8"); // 返回响应体
currentContext.setResponseBody(new ObjectMapper()
.writeValueAsString(new RespEntity(500, "先登录", null)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
}
其他:
客户端提供者:
package com.sms.controller;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:31
*/
@RefreshScope
@RestController
@RequestMapping("/student")
public class StudentController {
@Value("${server.port}")
private Integer port;
@Value("${testconfig}")
private String testconfig;
@GetMapping("/testconfig")
public RespEntity testconfig() {
return new RespEntity(2, "testconfig:"+port, testconfig);
}
@GetMapping("/{id}")
public RespEntity getStudent(@PathVariable("id") Integer id) {
if (id == 1) {
return new RespEntity(1, "ok"+port, new Student(1, "test1", 12));
}
return new RespEntity(2, "ok"+port, new Student(2, "test2", 11));
}
@GetMapping("/")
public RespEntity getAllStudent() {
return new RespEntity(2, "ok"+port, Arrays.asList(
new Student(1, "test1", 12),
new Student(2, "test2", 11),
new Student(3, "test3", 13)
));
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println(student);
return new RespEntity(200, "ok"+port, null);
}
}
客户端消费者:
package com.sms.controller;
import com.sms.FeignClient.IStudentFeignClient;
import com.sms.entity.RespEntity;
import com.sms.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-23 17:44
*/
@RefreshScope
@RestController
@RequestMapping("/feignstudent")
public class StudentFeignController {
@Autowired
IStudentFeignClient iStudentFeignClient;
@Value("${testconfig}")
private String testconfig;
@GetMapping("/{id}")
public RespEntity getStuInfo(@PathVariable("id") Integer id) {
System.out.println("获取一个学生信息");
return iStudentFeignClient.getStudent(id);
}
@GetMapping("/")
public RespEntity getAllStudent() {
System.out.println("获取所有学生信息");
return iStudentFeignClient.getAllStudent();
}
@PostMapping("/")
public RespEntity addStudent(@RequestBody Student student) {
System.out.println("添加学生");
System.out.println(student);
return iStudentFeignClient.addStudent(student);
}
}
Config
概述:
配置中心
- 对所有微服务的配置文件中统一管理
- 配置文件在git上
- 微服务通过配置中心获取git的地址,就可以加载自己的配置文件
- 修改配置文件,修改git仓库中的文件
代码:
创建git/gitee管理仓库
server-4041-pom:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!--config服务端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
server-4041-启动类:
package com.sms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-29 16:25
*/
@EnableEurekaClient
@EnableConfigServer
@SpringBootApplication
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}
server-4041-yml:
server:
port: 4041
spring:
application:
name: config
cloud:
config:
server:
git:
# git仓库地址
uri: https://gitee.com/sms1234/config.git
eureka:
client:
registerWithEureka: true # 是否将自己挂到注册中心上,单节点可以false,集群true
fetchRegistry: true # 是否要去注册获取服务列表
serviceUrl:
# defaultZone: http://localhost:6061/eureka/,http://localhost:6062/eureka/ #Eureka服务地址
defaultZone: http://localhost:6061/eureka/ #Eureka服务地址
其它微服务的配置文件上传到git:
每个微服务默认的配置文件都是 application.yml
传到git安装规范命名 consumserstudent-dev.yml
命名规范
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
http://localhost:4041/master/consumerstudent-dev.yml
http://localhost:4041/consumerstudent/dev/master
http://localhost:4041/consumerstudent-dev.yml
配置中心客户端-7071-pom:
<!--配置中心客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
配置中心客户端-7071-yml:
不需要使用application.yml,使用bootstrap.yml
bootstrap.yml 系统级别的配置文件(添加)
application.yml 用户级别的配置文件(注释,或者删除)
spring:
cloud:
config:
label: master
name: consumerstudent
profile: dev
uri: http://localhost:4041
#http://localhost:4041/master/consumerstudent-dev.yml
# 暴露刷新头
management:
endpoints:
web:
exposure:
include: "*"
修改配置文件,不需要重启服务,及时刷新配置文件
pom:
<!--监控插件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yml:
spring:
cloud:
config:
label: master
name: consumerstudent
profile: dev
uri: http://localhost:4041
#http://localhost:4041/master/consumerstudent-dev.yml
# 暴露刷新头
management:
endpoints:
web:
exposure:
include: "*"
controller添加刷新头:
@RefreshScope // 刷新注解
@RestController
@RequestMapping("/feignstudent")
public class StudentFeignController {}
在git/gitee修改配置文件后,发post请求,通知微服务及时刷新,不用重启服务!【需手动刷新】
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9anYeffU-1665143060454)(C:\Users\29295\AppData\Roaming\Typora\typora-user-images\image-20220929170537019.png)]
Bus
概述:
消息总线,微服务统一刷新【一般与Config一起使用】
curl -X POST http://localhost:4041/actuator/bus-refresh
只刷新指定服务
curl -X POST http://localhost:4041/actuator/bus-refresh/服务名:端口
代码:
4041-pom:
<!-- bus-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--监控插件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
4041-yml:
#消息队列
rabbitmq:
host: 192.168.247.153
port: 5672
username: guest
password: guest
#设置刷新头
management:
endpoints:
web:
exposure:
include: bus-refresh
7071/8081:
pom:
<!-- bus-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--监控插件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yml:
spring:
cloud:
config:
label: master
name: consumerstudent
profile: dev
uri: http://localhost:4041
#http://localhost:4041/master/consumerstudent-dev.yml
#消息队列
rabbitmq:
host: 192.168.247.153
port: 5672
username: guest
password: guest
# 暴露刷新头
management:
endpoints:
web:
exposure:
include: "*"
使用命令刷新:
curl -X POST http://localhost:4041/actuator/bus-refresh
Gateway
概述:官网
代码:
pom:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
yml:
#网关引入注册中心,同样可以通过服务名去注册中心获取服务地址
spring:
application:
name: gateway
cloud:
gateway:
routes: #路由配置
- id: provider-student #路由id,名字唯一,建议使用微服务名字
# uri: http://localhost:8081 #映射路径
uri: lb://provider-student
predicates: #断言;判断是否满足指定的条件
- Path=/student/** #请求路径
- id: consumer-student #路由id,名字唯一,建议使用微服务名字
# uri: http://localhost:7071 #映射路径
uri: lb://consumer-student
# - AddRequestHeader=h2, v2
# - AddResponseHeader=test, Blue
predicates: #断言;判断是否满足指定的条件
- Path=/** #请求路径
# - Header=h1, \d+
# - Cookie=a, ch.p
# - After=2022-09-30T11:55:19.880+08:00[Asia/Shanghai]
eureka:
client:
registerWithEureka: true # 是否将自己挂到注册中心上,单节点可以false,集群true
fetchRegistry: true # 是否要去注册获取服务列表
serviceUrl:
# defaultZone: http://localhost:6061/eureka/,http://localhost:6062/eureka/ #Eureka服务地址
defaultZone: http://localhost:6061/eureka/ #Eureka服务地址
server:
port: 1011
Route Predicate Factories
断言:就是条件
GatewayFilter Factories
Authentication 认证 401
Authorization 授权 403
自定义全局过滤器
package com.sms.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-30 15:59
*/
@Component
public class LoginFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("进入LoginFilter");
ServerHttpRequest request = exchange.getRequest();
// request.getParameter() key--value
// request.getParameterValues() key--v1,v2
// request.getQueryParams().get("user") 获取参数
if (request.getQueryParams().get("user") == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 返回状态码
System.out.println("拦截");
return exchange.getResponse().setComplete();
}
System.out.println("放行");
return chain.filter(exchange);
}
/**
* 级别
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
网关自定义响应体
package com.sms.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sms.entity.RespEntity;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @Author songmingsong
* @Version V1.0.0
* @Date 2022-09-30 15:59
*/
@Component
public class LoginFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("进入LoginFilter");
ServerHttpRequest request = exchange.getRequest();
// request.getParameter() key--value
// request.getParameterValues() key--v1,v2
// request.getQueryParams().get("user") 获取参数
if (request.getQueryParams().get("user") == null) {
System.out.println("拦截");
// exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 返回状态码
// return exchange.getResponse().setComplete();
// 返回前端统一响应
return exchange.getResponse().writeWith(Mono.just(result(exchange)));
}
System.out.println("放行");
return chain.filter(exchange);
}
/**
* 过滤器级别
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
/**
* 封装前端统一返回对象数据
*
* @param exchange
* @return
*/
public DataBuffer result(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
RespEntity responseResult = new RespEntity(401, "login first", "先登录");
byte[] bytes = null;
try {
bytes = new ObjectMapper()
.writeValueAsString(responseResult).getBytes(StandardCharsets.UTF_8);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);
response.getHeaders().add("Content-Type", "application/json;charset=utf-8");
return dataBuffer;
}
}
跨域处理
概述:
代码:
yml:
spring:
application:
name: gateway
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true #option跨域
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTION
allowedHeaders: "*"