menu ChaYedan
Spring Cloud从入门到入土
468 浏览 | 2020-04-28 | 阅读时间: 约 16 分钟 | 分类: Java | 标签: Java,spring
请注意,本文编写于 884 天前,最后修改于 884 天前,其中某些信息可能已经过时。

简介

Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

版本的选择

注意

SpringCloud官方网站上标明了SpringCloud与SpringBoot之间的关系

Release TrainBoot Version
Hoxton2.2.x
Greenwich2.1.x
Finchley2.0.x
Edgware1.5.x
Dalston1.5.x

使用时建议对应使用,并且使用带GA版本和SRX(数字)版本

注册中心 Spring Cloud Eureka Demo

说到Eureka啊,我就想到了交响诗篇(


扯远了。。。回到正题

Eureka是网飞出的框架,详情请见https://spring.io/projects/spring-cloud-netflix#overview

Eureka解决了服务的管理,注册和发现、状态监管、动态路由的问题。并负责记录了服务提供者的信息。Eureka将提供者的信息会匹配给服务消费者,且在提供者之间定时确定服务提供者的状态(http方式)。服务消费者也会定时从Eureka中拉取服务列表。

整合注册中心Eureka

搭建工程Eureka工程

勾选坐标

在启动类中使用@EnableEurekaServer注解
@SpringBootApplication
@EnableEurekaServer //声明当前应用为Eureka服务中心
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }

}
编写配置文件application.yml
server:
  port: 10010 # 端口
spring:
  application:
    name: eureka-server # 当前应用名称,会在Eureka中作为服务的id标识(serviceId)
eureka:
  client:
    service-url: # EurekaServer的地址
      defaultZone: http://127.0.0.1:10010/eureka
    # 抓取注册列表
    fetch-registry: false
    # 是否注册服务中心Eureka
    register-with-eureka: false
启动EurekaServerApplication

访问地址http://127.0.0.1:10010

注册服务提供者/消费者

添加Eureka客户端依赖

pom文件中添加依赖,其中<dependencyManagement>可以整合到父工程中去

<!--eureka客户端starter-->
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>
<!--SpringCloud所有依赖管理的坐标-->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
在启动类上开启Eureka客户端发现功能
@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端发现功能
public class ConsumerServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerServiceApplication.class, args);
    }
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
修改配置文件

根据消费者和服务提供者修改成对应的配置文件

# 配置应用基本信息
server:
  port: 8080
spring:
  application:
    name: user-service

# 配置eurekaserver
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10010/eureka

修改完成后,启动。在Eureka中可以看到服务

消费者通过Eureka访问提供者

消费者示例代码

@RestController
@RequestMapping("/consumer")
public class ConsumerController {
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("{id}")
    public User queryById(@PathVariable Long id){
        String url = String.format("http://localhost:9091/user/%d", id);

        //1、获取Eureka中注册的user-service实例列表
        List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("user-service");
        //2、获取实例
        ServiceInstance serviceInstance = serviceInstanceList.get(0);
        //3、根据实例的信息拼接的请求地址
        url = String.format("http://%s:%s/user/%d", serviceInstance.getHost(), serviceInstance.getPort(), id);
        //发生请求
        return restTemplate.getForObject(url,User.class);
    }
}

其中通过

@Autowired
private DiscoveryClient discoveryClient;

来获取服务列表

List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("user-service");

然后在列表中获取需要的那个服务

ServiceInstance serviceInstance = serviceInstanceList.get(0);
url = String.format("http://%s:%s/user/%d", serviceInstance.getHost(), serviceInstance.getPort(), id);

serviceInstanceList.get(0);是因为这里的服务名称可能重名,在getInstances("user-service");时返回的是一个列表,所以需要列表中的哪一个就写get哪一个

详解过程

Eureka架构中的三个核心角色

  • 服务注册中心:Eureka服务端应用,提供服务注册发现功能,eureka-server
  • 服务提供者:提供服务的应用,要求统一对外提供Rest风格服务即可
  • 服务消费者:从注册中心获取服务列表

Eureka客户端

服务提供者要向注册中心注册服务,并完成服务续约等工作。在配置好依赖坐标和配置文件后,在启动时会检测是否有@EnableDiscoveryClient和配置信息。有就向注册中心发起注册请求,携带服务元数据信息。Eureka注册中心会把服务的信息保存在Map中。

服务每隔30秒会向注册中心续约一次,如果没有续约,租约在90秒后到期,然后服务会被失效。每隔30秒的续约操作我们称之为:心跳检测。

#向Eureka服务中心集群注册服务
eureka:
  instance:
   # 租约续约间隔时间,默认30秒
    lease-renewal-interval-in-seconds: 30 
      # 租约到期,服务时效时间,默认值90秒
    lease-expiration-duration-in-seconds: 90 
   

服务超过90秒没有发生心跳,Eureka会将服务从列表移除。

消费者每隔30秒服务会从注册中心中拉取一份服务列表。时间可以通过配置修改

#向Eureka服务中心集群注册服务
eureka:
  client:
      # 每隔多久获取服务中心列表,(只读备份)
    registry-fetch-interval-seconds: 30 

服务消费者启动时,会检测是否获取服务注册信息配置。如果是,则会从 EurekaServer服务列表获取只读备份,缓存到本地。每隔30秒,会重新获取并更新数据。

其实我们常说是消费者和服务提供者,这是站在我们知道我们写的是什么的角度。但其实他们都开启了@EnableDiscoveryClient。那么对于Erueka来说,他们即是消费者又是服务提供者。

失效剔除和自我保护

服务下线

  • 当服务正常关闭操作时,会发送服务下线的REST请求给EurekaServer。
  • 服务中心接受到请求后,将该服务置为下线状态

失效剔除

服务中心每隔一段时间(默认60秒)将清单中没有续约的服务剔除。

通过eviction-interval-timer-in-ms配置可以对其进行修改,单位是毫秒

自我保护

Eureka会统计服务实例最近15分钟心跳续约的比例是否低于85%,如果低于则会触发自我保护机制。

  • 自我保护模式下,不会剔除任何服务实例
  • 自我保护模式保证了大多数服务依然可用
  • 通过enable-self-preservation配置可用关停自我保护,默认值是打开

    #向Eureka服务中心集群注册服务
    eureka:
      server:
        enable-self-preservation: false # 关闭自我保护模式(缺省为打开)

负载均衡 Spring Cloud Ribbon

Ribbon是Netflix发布的负载均衡器,有助于控制HTTP客户端行为。为Ribbon配置服务提供者地址列表后,Ribbon就可基于负载均衡算法,自动帮助服务消费者请求。其中轮询是默认的负载均衡算法。

Demo

Eureka已经集成Ribbon,无需引入依赖。

开启消费者调用负载均衡,在RestTemplate的配置方法上添加@LoadBalanced注解即可

@Bean
@LoadBalanced//开启负载均衡
public RestTemplate restTemplate(){
    return new RestTemplate();
}

修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用(下面代码中的user-service)

@GetMapping("{id}")
public User queryById(@PathVariable int id){
    String url = String.format("http://user-service/user/%d", id);
    return restTemplate.getForObject(url,User.class);
}

配置修改轮询策略

# 修改服务地址轮询策略,默认是轮询,配置之后变随机
user-service:
ribbon:
 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

在消费者里的配置改

user-service是服务的ID

熔断器 Spring Cloud Hystrix

熔断器是一种保护机制,Hystrix也是网飞的一款组件。这套组件能在服务提供者的如果出现故障时,能够及时发现。并隔离访问远程服务、第三方库、防止出现级联失败也就是雪崩效应

雪崩效应

  • 微服务中,一个请求可能需要多个微服务接口才能实现,会形成复杂的调用链路。
  • 如果某服务出现异常,请求阻塞,用户得不到响应,容器中线程不会释放,于是越来越多用户请求堆积,越来越多线程阻塞。
  • 单服务器支持线程和并发数有限,请求如果一直阻塞,会导致服务器资源耗尽,从而导致所有其他服务都不可用,从而形成雪崩效应;

Hystrix解决雪崩问题的手段,主要是服务降级(兜底),线程隔离。

Demo

引入熔断的依赖坐标

<!--熔断Hystrix starter-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

开启熔断的注解

@SpringBootApplication
@EnableDiscoveryClient//开启服务发现
@EnableCircuitBreaker//开启熔断
public class ConsumerApplication {
    @Bean
    @LoadBalanced//开启负载均衡
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class,args);
    }
}

注解简化写法:微服务中,注解往往引入多个,简化注解可以使用组合注解。

@SpringCloudApplication =@SpringBootApplication+@EnableDiscoveryClient+@EnableCircuitBreaker

编写服务降级处理方法:使用@HystrixCommand定义fallback方法。

@RequestMapping("{id}")
@HystrixCommand(fallbackMethod ="queryByIdFallback")
public String queryById(@PathVariable Long id){
    String url = String.format("http://user-service/user/%d", id);
    return restTemplate.getForObject(url,String.class);
}

public String queryByIdFallback(Long id){
    return "对不起,网络太拥挤了!";
}

原理分析

熔断器状态机有3个状态:

  • 关闭状态,所有请求正常访问
  • 打开状态,所有请求都会被降级。

    • Hystrix会对请求情况计数,当一定时间失败请求百分比达到阈值,则触发熔断,断路器完全关闭
    • 默认失败比例的阈值是50%,请求次数最低不少于20次
  • 半开状态

    • 打开状态不是永久的,打开一会后会进入休眠时间(默认5秒)。休眠时间过后会进入半开状态。
    • 半开状态:熔断器会判断下一次请求的返回状况,如果成功,熔断器切回关闭状态。如果失败,熔断器切回打开状态。

熔断器的核心解决方案:线程隔离和服务降级

  • 线程隔离。
  • 服务降级(兜底方法)

线程隔离和服务降级之后,用户请求故障时,线程不会被阻塞,更不会无休止等待或者看到系统奔溃,至少可以看到执行结果(熔断机制)。

什么时候熔断

  1. 访问超时
  2. 服务不可用(死了)
  3. 服务抛出异常(虽然有异常但还活着)
  4. 其他请求导致服务异常到达阈值,所有服务都会被降级

模拟异常测试:http://localhost:8080/consumer/1失败请求发送10次以上。再请求成功地址http://localhost:8080/consumer/2,发现服务被熔断,会触发消费者fallback方法。

配置熔断策略

# 配置熔断策略:
hystrix:
  command:
    default:
      circuitBreaker:
        # 强制打开熔断器 默认false关闭的。测试配置是否生效
        forceOpen: false
        # 触发熔断错误比例阈值,默认值50%
        errorThresholdPercentage: 50
        # 熔断后休眠时长,默认值5秒
        sleepWindowInMilliseconds: 5000
        # 熔断触发最小请求次数,默认值是20
        requestVolumeThreshold: 10
      execution:
        isolation:
          thread:
            # 熔断超时设置,默认为1秒
            timeoutInMilliseconds: 2000

熔断触发最小请求次数和触发熔断错误比例阈值是一对,当超过熔断触发最小请求次数后并且达到触发熔断错误比例阈值后才会触发熔断。然后进入打开状态。即熔断。

休息规定的熔断后休眠时长后,进入半开状态,即发送请求,成功则中止熔断,恢复正常服务。失败则继续保持熔断再等待规定的熔断后休眠时长后发起请求。

服务降级的fallback方法

两种编写方式:编写在类上,编写在方法上。在类的上边对类的所有方法都生效。在方法上,仅对当前方法有效。

  1. 方法上服务降级的fallback兜底方法

    • 使用HystrixCommon注解,定义
    • @HystrixCommand(fallbackMethod="queryByIdFallBack")用来声明一个降级逻辑的fallback兜底方法
  2. 类上默认服务降级的fallback兜底方法

    • 刚才把fallback写在了某个业务方法上,如果方法很多,可以将FallBack配置加在类上,实现默认FallBack,但要注意的是默认的服务降级的fallback兜底方法的参数列表必须为空。
    • @DefaultProperties(defaultFallback=”defaultFallBack“),在类上,指明统一的失败降级方法;

因为熔断的降级逻辑方法跟正常逻辑方法一样,必须保证相同的参数列表和返回值相同。

Spring Cloud Feign

简介

Feign vt. 假装;装作;

Feign是一个http请求调用的轻量级框架。,是以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。

项目主页

https://github.com/OpenFeign/feign

个人理解

其实我自己理解来看,Spring Cloud Feign就是程序员们为了约定成俗的习惯而捣鼓出来的玩意。

在以往,也就是我以前写过的应用框架演进中的垂直应用架构时期,我们的Controller可以直接调用Service来完成相关业务。但到了分布式服务架构的时候,我们的Controller和Service是分开部署的,不能直接像以往那么直接在代码里调用了,需要借助注册中心中拉取到的服务列表来进行调用。

比如

@RestController
@RequestMapping("/consumer")
public class ConsumerController {
  @Autowired
  private RestTemplate restTemplate;
  @Autowired
  private DiscoveryClient discoveryClient;
  @GetMapping("{id}")
  public User queryById(@PathVariable Long id){
    // 没有用注册中心的调用形式
    String url = String.format("http://localhost:9091/user/%d", id);
    // 使用注册中心的调用形式
    List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("user-service");
    ServiceInstance serviceInstance = serviceInstanceList.get(0);
    url = String.format("http://%s:%s/user/%d", serviceInstance.getHost(),serviceInstance.getPort(), id);
    return restTemplate.getForObject(url,User.class);
 }
}

可以看出,这对于我们写Controller时是十分的不“约定成俗”,也就是我们习惯的直接调用Service来处理。

想不起来?就像这样

@RestController
@RequestMapping("/consumer")
public class ConsumerController {
  @Autowired
  private ConsumerService consumerService;
  
  @GetMapping("{id}")
  public User queryById(@PathVariable Long id){
    return consumerService.queryById(id);
 }
}

看到这是不是就很熟悉了呢?回到正题

那么怎么样才能还原成我们的这样直接调用Service呢?这就是Feign解决的问题。也正是项目组取名的那样,假装我们就是在一个工程里面的!!!来看看下面的Demo

Demo

导入依赖

添加 spring-cloud-starter-openfeign 依赖

<!--配置feign-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

这里不写版本的原因

在上面引入

<!--SpringCloud所有依赖管理的坐标-->
 <dependencyManagement>
     <dependencies>
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-dependencies</artifactId>
             <version>Greenwich.SR1</version>
             <type>pom</type>
             <scope>import</scope>
         </dependency>
     </dependencies>
 </dependencyManagement>

里面集成了版本号

Feign的客户端

consumer_service中编写Feign客户端接口类UserService。看到("user-service")了吗?看到"/user/findById"了吗?这就是把这个接口的链接到user-service服务上去了。

@FeignClient("user-service")//指定feign调用的服务
public interface UserService {
  @RequestMapping("/user/findById")
  User findById(@RequestParam("id") Integer id);
}

名为user-service服务提供者的代码

UserService实现类

@Service
public class UserServiceImpl implements UserService {
  @Autowired
  private UserMapper userMapper;
  @Override
  public List<User> findAll() {
    return userMapper.findAll();
 }
   @Override
  public User findById(Integer id) {
    return userMapper.findById(id);
 }
}

UserController

@RestController
@RequestMapping("/user")
public class UserController {
  @Autowired
  UserService userService;
  /**
  * 查询所有
  * @return
  */
  @RequestMapping("/findAll")
  public List<User> findAll() {
    return userService.findAll();
 }
  /**
  * 根据id查询
  * @param id
  * @return
  */
  @RequestMapping("/findById")
  public User findById(Integer id) {
    return userService.findById(id);
 }
}

这下懂了吧,这就是伪装。在我consumer_service的Controller里,我就可以用

UserService.findById(id)

来调用user-service中注册的服务了!!!


​ 回到正题

  • Feign会通过动态代理,帮我们生成实现类。
  • 注解@FeignClient声明Feign的客户端,指明服务名称
  • 接口定义的方法,采用SpringMVC的注解。Feign会根据注解帮我们生成URL地址

编写ConsumerFeignController,使用userService访问

@RestController
public class ConsumerController {
 @Autowired
 UserService userService;

 @RequestMapping("/feignconsumer/{id}")
 public User hellofeign(@PathVariable Integer id){
     return userService.findById(id);
 }
}

开启Feign功能

ConsumerApplication启动类上,添加 @EnableFeignClients 注解,开启Feign功能。

Feign中已经自动集成Ribbon负载均衡,因此不需要自定义RestTemplate进行负载均衡的配置

负载均衡

Feign本身集成了Ribbon,因此不需要额外引入依赖,也不需要再注册RestTemplate对象。

熔断器支持

Feign本身也集成Hystrix熔断器,starter内查看。

Feign中服务降级方法实现步骤

  1. 在配置文件application.yml中开启feign熔断器支持,默认关闭

    feign:
        hystrix:
            enabled: true # 开启Feign的熔断功能
  2. 编写FallBack处理类,实现FeignClient客户端

    @Component//需要注意:一定要注入Spring 容器
    public class UserServiceFallBack implements UserService {
      @Override
      public User findById(Integer id) {
        User user = new User();
        user.setId(id);
        user.setUsername("用户不存在!!!");
        return user;
     }
    }
  3. 在@FeignClient注解中,指定FallBack处理类。

    @FeignClient(value = "user-service",fallback = UserServiceFallBack.class)
    public interface UserService {
      @RequestMapping("/user/findById")
      User findById(@RequestParam("id") Integer id);
    }
  4. 测试服务降级效果

请求压缩和响应压缩

SpringCloudFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。

通过配置开启请求与响应的压缩功能:

feign:
compression:
   request:
     enabled: true # 开启请求压缩
   response:
     enabled: true # 开启响应压缩

也可以对请求的数据类型,以及触发压缩的大小下限进行设置

# Feign配置
feign:
    compression:
        request:
            enabled: true # 开启请求压缩
            mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
            min-request-size: 2048 # 设置触发压缩的大小下限
            #以上数据类型,压缩大小下限均为默认值

配置日志级别

在发送和接收请求的时候,Feign定义了日志的输出定义了四个等级:这里我们配置测试一下。

级别说明
NONE不做任何记录
BASIC只记录输出Http 方法名称、请求URL、返回状态码和执行时间
HEADERS记录输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息
FULL记录Request 和Response的Header,Body和一些请求元数据

实现步骤

  1. 在application.yml配置文件中开启日志级别配置

在consumer_service的配置文件中设置com.chayedan包下的日志级别都为debug

# com.chayedan 包下的日志级别都为Debug
logging:
    level:
         com.chayedan: debug
  1. 编写配置类,定义日志级别bean。

    @Configuration
    public class FeignConfiguration {
      @Bean
      public Logger.Level feignLoggerLevel(){
        //记录所有请求和响应的明细,包括头信息,请求体,元数据
        return Logger.Level.FULL;
     }
    }
  2. 在接口的@FeignClient中指定配置类

    @FeignClient(value="user-service",fallback =
    UserClientFallBack.class,configuration = FeignConfig.class)
    public interface UserClient {
      @RequestMapping("/user/{id}")
      User queryById(@PathVariable("id") Long id);
    }
  3. 重启项目,测试访问,即可看到每次访问的日志

Spring Cloud Gateway 网关

简介

Spring Cloud Gateway 是Spring Cloud团队的一个全新项目,基于Spring 5.0、SpringBoot2.0、Project Reactor 等技术开发的网关。 旨在为微服务架构提供一种简单有效统一的REST 请求路由管理方式

Spring Cloud Gateway 作为SpringCloud生态系统中的网关,目标是替代Netflix Zuul。Gateway不仅提供统一路由方式,并且基于Filter链的方式提供网关的基本功能。例如:安全,监控/指标,和限流。

本身也是一个微服务,需要注册到Eureka

网关的核心功能:过滤(权限)、路由

核心概念

  • 路由(route):
  • 断言Predicate函数:路由转发规则
  • 过滤器(Filter):
  • 不管是来自客户端的请求,还是服务内部调用。一切对服务的请求都可经过网关。
  • 网关实现鉴权、动态路由等等操作。
  • Gateway是服务的统一入口

Demo

创建SpringBoot工程gateway_server,勾选网关、Eureka客户端

启动引导类开启注册中心Eureka客户端发现

@SpringBootApplication
@EnableDiscoveryClient// 开启Eureka客户端发现功能
public class GatewayApplication {
  public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class,args);
 }
}

编写基础配置

在gateway_server中创建application.yml文件

server:
    port: 10010
spring:
    application:
         name: api-gateway # 应用名
    # Eureka服务中心配置
eureka:
    client:
         service-url:
               # 注册中心Eureka服务地址
              defaultZone: http://127.0.0.1:10086/eureka

编写路由规则

需要用网关来路由user_service服务,在注册中心查看服务ip和端口。修改gateway_server的配置文件application.yml,配置网关内容

spring:
cloud:
 gateway:
   # 路由si(集合)
  routes:
     # id唯一标识
   - id: user-service-route
     # 路由服务地址
     uri: http://127.0.0.1:9091
     # 断言
     predicates:
       - Path=/user/**

将符合 path 规则的请求,路由到 uri 参数指定地址。在这个Demo中是将http://localhost:10010/user/findById?id=1 路由转发到http://localhost:9091/user/fi
ndById?id=1

注意点

id前面有- 代表这是下面的是一个数组元素正题,而且下面写的是uri地址。别手滑写成url 了

断言那里的写的是/**

动态路由

刚才路由规则中,我们把路径对应服务地址写死了!如果服务提供者集群的话,这样做不合理。应该是根据服务名称,去Eureka注册中心查找服务对应的所有实例列表,然后进行动态路由!

修改映射配置:通过服务名称获取

因为已经配置了Eureka客户端,可以从Eureka获取服务的地址信息,修改application.yml文件如下

# 注解版
spring:
    cloud:
       gateway:
           # 路由si(集合)
          routes:
             # id唯一标识
           - id: user-service-route
             # 路由地址
             # uri: http://127.0.0.1:9091
             # 采用lb协议,会从Eureka注册中心获取服务请求地址
             # 路由地址如果通过lb协议加服务名称时,会自动使用负载均衡访问对应服务
             # 规则:lb协议+服务名称
             uri: lb://user-service
             # 路由拦截地址(断言)
             predicates:
               - Path=/user/**

路由配置中uri所用的协议为lb时,gateway将把user-service解析为实际的主机和端口,并通过Ribbon进行负载均衡。

路由前缀

添加前缀

在gateway中可以通过配置路由的过滤器PrefixPath 实现映射路径中的前缀添加。可以起到隐藏接口地址的作用,避免接口地址暴露。

例如

spring:
cloud:
 gateway:
  routes:
   - id: user-service-route # 路由id,可以随意写
     # 代理服务地址;lb表示从Eureka中获取具体服务
     uri: lb://user-service
     # 路由断言,配置映射路径
     predicates:
      - Path=/**
     # 请求地址添加路径前缀过滤器
     filters:
      - PrefixPath=/user

就等同于访问地址localhost:10010/findById?id=1->localhost:9091/user/findById?id=1

去除前缀

在gateway中通过配置路由过滤器StripPrefix,实现映射路径中地址的去除。通过StripPrefix=1来指定路由要去掉的前缀个数。如:路径/api/user/1将会被路由到/user/1。

例如

spring:
cloud:
 gateway:
  routes:
   - id: user-service-route # 路由id,可以随意写
     # 代理服务地址;lb表示从Eureka中获取具体服务
     uri: lb://user-service
     # 路由断言,配置映射路径
     predicates:
       - Path=/**
     # 去除路径前缀过滤器
     filters:
       - StripPrefix=1

效果等同于localhost:10010/api/user/findById?id=1->localhost:9091/user/findById?id=1

过滤器

过滤器作为网关的其中一个重要功能,就是实现请求的鉴权。前面的路由前缀的功能也是使用过滤器实现的。

Gateway自带过滤器有几十个,常见自带过滤器有:

过滤器名称说明
AddRequestHeader对匹配上的请求加上Header
AddRequestParameters对匹配上的请求路由
AddResponseHeader对从网关返回的响应添加Header
StripPrefix对匹配上的请求路径去除前缀
PrefixPath对匹配上的请求路径添加前缀

使用场景

  • 请求鉴权:如果没有访问权限,直接进行拦截
  • 异常处理:记录异常日志
  • 服务调用时长统计

更多过滤器详情:https://cloud.spring.io/spring-cloud-gateway/reference/html/#gatewayfilter-factories

过滤器配置

过滤器类型:Gateway有两种过滤器

  • 局部过滤器:只作用在当前配置的路由上。
  • 全局过滤器:作用在所有路由上。
配置全局过滤器

修改配置文件

spring:
    cloud:
         gateway:
              # 配置全局默认过滤器
              default-filters:
                  # 往响应过滤器中加入信息
                   - AddResponseHeader=test,chayedan
配置局部过滤器

详见前面路由前缀的配置文件

自定义全局过滤器

@Component//注入Spring容器中
public class MyGlobalFiler implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //写拦截业务逻辑
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //业务逻辑:有token则放行
        MultiValueMap<String, String> queryParams = request.getQueryParams();//所有参数
        String token = queryParams.getFirst("token");

        if (StringUtils.isEmpty(token)){
            //设置响应状态码,提示用户未授权
            response.setStatusCode(HttpStatus.UNAUTHORIZED);

            //进行拦截
            return response.setComplete();//请求拦截
        }
        //怎么放行请求?
        return chain.filter(exchange);//放行请求
    }

    /**
     * 返回执行顺序,数字越小,执行顺序越靠前
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

其中GlobalFilter, Ordered都是Spring自带的接口。

Spring Cloud Config

分布式系统中,由于服务数量非常多,配置文件分散在不同微服务项目中,管理极其不方便。为了方便配置文件集中管理,需要分布式配置中心组件。在Spring Cloud中,提供了Spring Cloud Config,它支持配置文件放在配置服务的本地,也支持配置文件放在远程仓库Git(GitHub、码云)。

配置中心本质上是一个微服务,同样需要注册到Eureka服务中心!

整合配置中心步骤

  1. 创建远程仓库
  2. 创建配置文件,在新建的仓库中创建需要被统一配置管理的配置文件

建议命名的方式

配置文件的命名方式:{application}-{profile}.yml或{application}-{profile}.properties

  • application为应用名称
  • profile用于区分开发环境dev,测试环境test,生产环境pro等

例如将user-service工程里的配置文件application.yml内容复制作为user-dev.yml文件内容

  1. 创建完保存配置文件即可
  2. 搭建配置中心微服务,勾选坐标Config Server,Eureka Discovery Client。创建好后在启动类添加

    @EnableDiscoveryClient//开启Eureka客户端发现功能
    @EnableConfigServer //开启配置服务支持
  3. 在配置文件application.yml中填写配置,例如

    # 应用基本信息
    server:
      port: 12000 # 端口号
    spring:
      application:
        name: config-server # 应用名
      cloud:
        config:
          server:
            git:
              # 配置gitee的仓库地址
              uri: 你的仓库地址
    # Eureka服务中心配置
    eureka:
      client:
        service-url:
          # 注册Eureka Server集群
          defaultZone: http://127.0.0.1:10086/eureka
    # com.chayedan 包下的日志级别都为Debug
    logging:
      level:
        com: debug

完成后启动,就能获取到远程仓库中关于你配置中心的配置了

服务去获取配置中心配置

改造user_service工程,配置文件不再由微服务项目提供,而是从配置中心获取。

实现步骤

  1. 添加user_service的starter依赖

    <!--spring cloud 配置中心-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
  2. 修改服务提供者的配置文件

删除user_service工程的application.yml文件,创建user_service工程bootstrap.yml配置文件,配置内容如下

# 基本应用信息
spring:
  cloud:
    config:
      name: user # 与远程仓库中的配置文件的application保持一致,{application}-{profile}.yml
      profile: dev # 远程仓库中的配置文件的profile保持一致
      label: master # 远程仓库中的版本保持一致
      discovery:
        enabled: true # 使用配置中心
        service-id: config-server # 配置中心服务id

#向Eureka服务中心集群注册服务
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

关于application.yml和bootstrap.yml文件的说明:

  • bootstrap.yml文件是SpringBoot的默认配置文件,而且其加载时间相比于application.yml更早。
  • bootstrap.yml和application.yml都是默认配置文件,但定位不同

    • bootstrap.yml相当于项目启动的引导文件
    • application.yml文件是微服务的常规配置参数,变化比较频繁
  • 搭配spring-cloud-config使application.yml的配置可以动态替换。
  1. 启动服务
  2. 测试效果

配置中心存在的问题

启动所有服务,修改远程Git配置后,发现并没有及时更新到相关微服务,只有重启用户微服务才能生效。因为配置只加载一次。

下面的Spring Cloud Bus就是来解决这个问题。

Spring Cloud Bus 消息总线

环境准备

安装好erlang

安装好RabbitMQ

RabbitMQ运行状态处于运行状态

Bus是用轻量的消息代理将分布式的节点连接起来,可以用于广播配置文件的更改或者服务的监控管理。

Bus可以为微服务做监控,也可以实现应用程序之间互相通信。Bus可选的消息代理RabbitMQ和Kafka。

广播出去的配置文件服务会进行本地缓存。

整合案例

目标:消息总线整合入微服务系统,实现配置中心的配置自动更新。不需要重启微服务。

改造配置中心

在config_server项目中加入Bus相关依赖

<!--消息总线依赖-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-bus</artifactId>
</dependency>
<!--RabbitMQ依赖-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>

在config_server项目中修改application.yml

# rabbitmq的配置信息;如下配置的rabbit都是默认值,其实可以完全不配置
spring:
    rabbitmq:
         host: localhost
         port: 5672
         username: guest
         password: guest
# 暴露触发消息总线的地址
management:
    endpoints:
         web:
            exposure:
            # 暴露触发消息总线的地址
               include: bus-refresh

改造用户服务

  1. 在用户微服务user_service项目中加入Bus相关依赖

    <!--消息总线依赖-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-bus</artifactId>
    </dependency>
    <!--RabbitMQ依赖-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
    </dependency>
  2. 修改user_service项目的bootstrap.yml,加入RabbitMQ的配置信息

    # rabbitmq的配置信息;如下配置的rabbit都是默认值,其实可以完全不配置
    spring:
        rabbitmq:
             host: localhost
             port: 5672
             username: guest
             password: guest
  3. UserController类上加入@RefreshScope刷新配置注解

    @RestController
    @RequestMapping("/user")
    @RefreshScope //刷新配置
    public class UserController {
      @Value("${server.port}")
      private String port;
      @Value("${test.hello}")
      private String name;
     
      @Autowired
      UserService userService;
      //查询所有
      @RequestMapping("/findAll")
      public List<User> findAll() {
        return userService.findAll();
     }
      //根据id查询
      @RequestMapping("/findById")
      public User findById(Integer id) {
        System.out.println("服务【"+port+"】被调用");
        User user = userService.findById(id);
        user.setNote("服务【"+port+"】被调用");
        user.setName(name);
        return user;
     }
    }
  4. 测试

    使用Postman工具发送POST请求,地址:http://127.0.0.1:12000/actuator/bus-refresh。刷新配置。

请求地址http://127.0.0.1:12000/actuator/bus-refresh中actuator是固定的,bus-refresh对应的是配置中心的config_server中的application.yml文件的配置项include的内容

消息总线实现消息分发过程:

  • 请求地址访问配置中心的消息总线
  • 消息总线接收到请求
  • 消息总线向消息队列发送消息
  • user-service微服务会监听消息队列
  • user-service微服务接到消息队列中消息后
  • user-service微服务会重新从配置中心获取最新配置信息
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,快来留言吧!