万字长文,小白必看-SpringCloud学习笔记

643 阅读5分钟

SpringCloud学习笔记

该学习笔记是根据千峰教育SpringCloud教程整理,

视频教程详见:www.bilibili.com/video/BV18Z…

一. SpringCloud介绍

1.1 微服务架构

微服务框架提出者:马丁福勒

martinfowler.com/articles/mi…

简而言之,微服务架构样式是一种将单个应用开发为一组小服务的方法,每个小服务都在自己的进程中运行并与轻量级机制(通常是http资源API)进行通讯。这些服务围绕业务功能构建,并且可以由全自动部署机制独立部署。这些服务的集中管理几乎没有,它可以使用不同的编程语言编写,并使用不同的数据存储技术。

  1. 微服务架构只是一种样式,一种风格
  2. 将一个完整的项目,拆分多个模块去分别开发
  3. 每个模块都是单独的运行在自己的容器中
  4. 每个模块都需要互相通信的。Http,RPC,MQ
  5. 每模块之间是没有依赖关系的,单独部署
  6. 可以使用多语言去开发不同的模块
  7. 使用MySQL数据,Redis,ES去存储数据,也可以使用多个MySQL数据库

总结:将复杂臃肿的单体应用进行细粒度的划分,每个拆分出来的服务各自打包部署。

1.2 SpringCloud介绍

SpringCloud是微服务架构落地的一套技术栈。

SpringCloud技术栈中大部分技术是基于Netflix公司的技术二次开发

1、SpringCloud中文网站:www.springcloud.cc/

八个技术点:

  1. Eureka - 服务的注册和发现
  2. Robbin - 服务之间的负载均衡
  3. Feign - 服务之间的通讯
  4. Hystrix - 服务的线程隔离以及断路器
  5. Zuul - 服务网关
  6. Stream - 实现MQ的使用
  7. Config - 动态配置
  8. Sleuth - 服务追踪

二.服务的注册与发现-Eureka

2.1引言

Eureka就是帮助我们维护所有的服务信息,以便服务之间的相互调用。

她解决了2个问题:

  1. 如果被调用方,ip或端口号改变了,需要维护调用方
  2. 如果被调用方搭建集群,还要去维护全部调用方

image-20201122142131868

2.2 Eureka的快速入门

2.2.1 创建EurekaServer

1、创建一个父工程,并且在父工程中指定SpringCloud的版本,并把packing改为pom

<packaging>pom</packaging>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring.cloud-version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2、创建Eureka的server:创建SpringBoot工程,并且导入依赖,在启动类中添加注解,编写yml文件

2.1、导入依赖

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2、启动类添加注解

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

2.3、编写yml配置文件

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://root:root@${eureka.instance.hostname}:${server.port}/eureka/
spring:
  security:
    user:
      name: root
      password: root
  application:
    name: EUREKA

2.2.2 创建EurekaClient

1、创建Maven工程,修改为SpringBoot

<parent>
<artifactId>springcloudstudy</artifactId>
<groupId>com.chtgeo</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>02-customer</artifactId>

2、导入依赖

<dependencies>
	<dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3、在启动类上添加注解

@SpringBootApplication
@EnableEurekaClient
public class CustomerApplication {
    public static void main(String[] args) {
        SpringApplication.run(CustomerApplication.class, args);
    }
}

4、编写配置文件

eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

spring:
  application:
    name: CUSTOMER

2.2.3 测试Eureka

1、创建一个Search搜索模块,并且注册到Eureka

2、使用EurekaClient对象去获取服务信息

@Autowired
private EurekaClient eurekaClient;

3、正常RestTemplate调用即可

@GetMapping("/customer")
public String customer() {

    // 1. 通过eurakaClient获取SEARCH服务的信息
    InstanceInfo search = eurekaClient.getNextServerFromEureka("SEARCH", false);
    // 2. 获取访问地址
    String homePageUrl = search.getHomePageUrl();

    // 3. 通过restTemplate访问
    String result = restTemplate.getForObject(homePageUrl + "/search", String.class);

    // 4. 返回
    return result;

}

image-20201122155619460

2.3 Eureka的安全性

实现Eureka认证

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、编写配置类

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().ignoringAntMatchers("/eureka/**");
        super.configure(http);
    }
}

3、编写配置文件

# 用户名和密码
spring:
  security:
    user:
      name: root
      password: root

4、其他服务向注册到Eureka时需要添加用户名和密码

eureka:
  client:
    serviceUrl:
      defaultZone: http://用户名:密码@localhost:8761/eureka/

2.4 Eureka的高可用

如果程序正常运行,突然Eureka宕机了。

1、如果调用方访问过一次被调用方,Eureka宕机不会影响调用功能。

2、如果调用方没有访问过被调用方,Eureka宕机就会造成当前功能不可用。

搭建Eureka高可用

1、准备多台Eureka

采用了复制的方式,删除iml和target文件,并且修改pom.xml中的项目名称,再给父工程添加一个module.

2、让服务注册到多台Eureka

eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

3、多台Eureka相互通讯

eureka:
  client:
    registerWithEureka: true # 注册到Eureka上
    fetchRegistry: true # 从Eureka拉去信息
    serviceUrl:
      defaultZone: http://root:root@localhost:8762/eureka/

2.5 Eureka的细节

1、EurekaClient启动时,将自己的注册信息注册到EurekaServer上,EurekaServer会存储上EurekaClient的注册信息。

2、当EurekaClient调用服务时,本地没有注册信息的缓存时,去EurekaServer获取注册信息。

3、EurekaClient会通过心跳的方式和EurekaServer进行连接。(默认30秒EurekaClient会发送一次心跳请求,如果超过90秒还没有发送心跳信息的话,EurekaServer就认为你宕机了,将当前的EurekaClient从注册表中移除)

eureka:
  instance:
    lease-renewal-interval-in-seconds: 30 #心跳的间隔
    lease-expiration-duration-in-seconds: 90 #多久没发送就认为你宕机了

4、EurekaClient每隔30秒去EurekaServer中去更新本地的注册表

eureka:
  client:
    registry-fetch-interval-seconds: 30 # 每隔多久去更新一下本地的注册表缓存信息

5、Eureka的自我保护机制,统计15分钟内,如果一个服务的心跳发送比率低于85%,EurekaServer就会开启自我保护机制。

  1. 不会从EurekaServer中去移除长时间没有收到心跳的服务
  2. EurekaServer还是可以正常提供服务的
  3. 网络比较稳定时,EurekaServer才会开始将自己的信息被其他节点同步过去
eureka:
  server:
    enable-self-preservation: true #开启自我保护机制

6、CAP定理:C-一致性(Consistency)、A-可用性(Availability)、P-分区容错性(Partition tolerance)。这三个特性在分布式环境下,只能满足2个,而且分区容错性在分布式环境下,是必须满足的,只能在AC之间权衡。

CP:保证了一致性,可能会造成你系统在一定时间内是不可用的,如果你同步数据时间比较长,造成的损失大。

AP:保证可用性,Eureka就是一个AP的效果,高可用的集群,Eureka集群是无中心,Eureka即便宕机几个也不会影响系统的使用,不需要重新去推举一个master,也会导致一定时间内的数据不一致。

三.服务的负载均衡-Robbin

3.1 引言

Robbin是帮助我们实现服务和服务负载均衡,Robbin属于客户端负载均衡

客户端负载均衡:customer客户模块,将2个Search模块信息全部拉取到本地的缓存,在customer中自己做个负载均衡的策略,选中某一个服务。

服务端负载均衡:在注册中心,直接根据你指定的负载均衡策略,帮你选中一个指定的服务信息,并返回。

image-20201127113008008

3.2 Robbin的快速入门

1、启动2个Search模块

2、在customer导入2个Robbin模块

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

3、配合整合RestTemplate和Robbin

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}

4、在customer中访问search

@GetMapping("/customer")
public String customer() {
	String result = restTemplate.getForObject("http://SEARCH:8082/search", String.class);
    // 4. 返回
    return result;
}

3.3 Robbin配置负载均衡策略

1、负载均衡策略

  1. RandomRule:随机策略
  2. RoundRobbinRule:轮询策略
  3. WeightedResponseTimeRule:默认采用轮询策略,后续会根据服务的相应时间,自动给你分配权重
  4. BestAvailableRule:根据被调用方并发数最小的去分配

2、采用注解的形式

@Bean
public IRule ribbon() {
	return new BestAvailableRule();
}

3、 配置文件去指定负载均衡策略(推荐)

SEARCH: # 服务名称
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #具体负载均衡策略使用的类

四.服务之间的调用-Feign

4.1 引言

Feign可以帮我们实现面向接口编程,就是直接调用其他的服务,简化开发。

4.2 Feign的快速入门

1、导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、添加注解

@EnableFeignClients

3、创建一个接口,并和Search模块做映射

@FeignClient(value = "SEARCH") // 指定服务名称
public interface SearchClient {

    // value:目标服务器的请求路径,method:映射方式
    @RequestMapping(value = "/search", method = RequestMethod.GET)
    String search();
}

4、测试使用

@Autowired
private SearchClient searchClient;

@GetMapping("/customer")
public String customer() {
    String result = searchClient.search();
    return result;
}

4.3 Feign的传递参数方式

1、 注意事项

  1. 如果传递的参数比较复杂时,默认会采用POST的方式。
  2. 传递单个参数时,推荐使用@PathVariable,如果传递的单个参数比较多,可以采用@RequestParam,不要省略value属性。
  3. 传递对象信息时,统一采用json方式,添加@RequstBody
  4. Client接口必须采用@RequestMapping

2、在Search模块下准备三个接口

@GetMapping("/search/{id}")
public Customer findById(@PathVariable Integer id) {
	return new Customer(1, "张三", 23);
}

@GetMapping("/getCustomer")
public Customer getCustomer(@RequestParam Integer id, @RequestParam String name) {
	return new Customer(id, name, 23);
}

@PostMapping("/save")
	public Customer save(@RequestBody Customer customer) {
	return customer;
}

3、封装Customer模块下的Controller

@GetMapping("/customer/{id}")
public Customer findById(@PathVariable Integer id) throws InterruptedException {
    return customerService.findById(id);
}

@GetMapping("/getCustomer")
public Customer getCustomer(@RequestParam Integer id, @RequestParam String name) {
    return searchClient.getCustomer(id, name);
}

@GetMapping("/save") //会自动转换为POST请求  405
public Customer save(Customer customer) {
    return searchClient.save(customer);
}

4、再封装Client接口

@RequestMapping(value = "/search/{id}", method = RequestMethod.GET)
Customer findById(@PathVariable(value = "id") Integer id);

@RequestMapping(value = "/getCustomer", method = RequestMethod.GET)
Customer getCustomer(@RequestParam(value = "id") Integer id, @RequestParam(value = "name") String name);

@RequestMapping(value = "/save", method = RequestMethod.POST)
Customer save(@RequestBody Customer customer);

5、测试

4.4 Feign的Fallback

Fallback可以帮助我们在使用Feign调用另外一个服务时,如果出现了问题,走服务降级,返回一个错误数据,避免功能因为一个服务出现问题,全部失效。

1、 创建一个POJO类,实现Client接口。

@Component
public class SearchClientFallBack implements SearchClient {
    @Override
    public String search() {
        return "出问题了!!!";
    }

    @Override
    public Customer findById(Integer id) {
        return null;
    }

    @Override
    public Customer getCustomer(Integer id, String name) {
        return null;
    }

    @Override
    public Customer save(Customer customer) {
        return null;
    }
}

2、 修改Client接口中注解,添加一个属性。

@FeignClient(value = "SEARCH",fallback = SearchClientFallBack.class)

3、添加一个配置文件。

feign:
  hystrix:
    enabled: true

调用方无法知道具体的错误信息是什么,通过FallBackFactory的方式实现这个功能

1、 FallBackFactory基于Fallback

2、 创建一个POJO类,实现FallBackFactory

@Component
public class SearchClientFallBackFactory implements FallbackFactory<SearchClient> {

    @Autowired
    private SearchClientFallBack searchClientFallBack;

    @Override
    public SearchClient create(Throwable throwable) {
        throwable.printStackTrace();
        return searchClientFallBack;
    }
}

3、 修改Client接口中的属性

@FeignClient(value = "SEARCH",fallbackFactory = SearchClientFallBackFactory.class)

五.服务的隔离及断路器-Hystrix

5.1 引言

image-20201127150320617

5.2 降级机制实现

1、 导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

2、添加一个注解

@EnableCircuitBreaker

3、针对某个接口去编写他的降级方法

@GetMapping("/customer/{id}")
@HystrixCommand(fallbackMethod = "findByIdFallBack")
public Customer findById(@PathVariable Integer id) throws InterruptedException {
        int i = 1 / 0;
        return searchClient.findById(id);
}

// findById的降级方法,方法描述和接口一致
public Customer findByIdFallBack(@PathVariable Integer id) {
    return new Customer(-1, "", 0);
}

4、 在接口上添加注解

@HystrixCommand(fallbackMethod = "findByIdFallBack")

5、 测试一下

image-20201127151619729

5.3 线程隔离

如果用tomcat的线程池去接收用户的请求,使用当前线程去执行其他服务的功能,如果某个服务服务出现了故障了,会导致tomcat的线程出现大面积堆积,导致Tomcat无法处理其他业务功能。

1、Hystix线程池(默认),接收用户请求采用Tomcat的线程池,执行业务代码,调用其他服务时,采用Hystix的线程池。

2、 信号量,使用的还是Tomcat的线程池,帮助我们去管理Tomcat的线程池。

1、Hystix的线程池的配置(具体配置name属性需要去查看HystrixCommandProperties类)

  1. 线程隔离策略:name = execution.isolation.strategy, value = THREAD| SEMAPHORE
  2. 执行超时时间:name=circuitBreaker.enabled,value = true|false

image-20201127153539950

5.4 断路器

5.4.1 断路器介绍

在调用指定服务时,如果这个服务的失败率达到你输入的一个阀值,将断路器从close状态,转变为open状态,指定服务时被访问的,如果你访问就走fallback方法,在一定的时间内,open状态会再次转变为half open状态,允许一个请求发送到我的指定服务,如果成功,则转变为close,如果失败,服务再次转变为open状态,会再次循环到half open,直到断路器回到一个close状态。

image-20201128114833199

5.4.2 配置断路器的监控界面

1、导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

2、启动类中添加注解

@EnableHystrixDashboard

3、配置一个servlet

@WebServlet("/hystrix.stream")
	public class HystrixServlet extends HystrixMetricsStreamServlet {
}
// 在启动类上添加扫描servlet注解
@ServletComponentScan({"com.chtgeo.servlet"})

4、测试

直接访问:http://host:port/hystrix

image-20201128120001859

在当前位置输入映射好的servlet路径

image-20201128120423166

5.4.3 配置断路器的属性

断路器的属性

查找相关属性地址:github.com/Netflix/Hys…

  1. 断路器的开关:name = hystrix.command.default.circuitBreaker.enabled, value = true
  2. 失败阀值的总请求数:name=hystrix.command.default.circuitBreaker.requestVolumeThreshold,value=20
  3. 请求总数失败率到达%多少时:name=hystrix.command.default.circuitBreaker.errorThresholdPercentage,value=50
  4. 强制让服务拒绝请求:name=hystrix.command.default.circuitBreaker.forceOpen,value=false
  5. 强制让服务接收请求:name=hystrix.command.default.circuitBreaker.forceClosed,value=false

具体配置方式

@GetMapping("/customer/{id}")
@HystrixCommand(fallbackMethod = "findByIdFallBack", commandProperties = {
    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "70"),
    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),
})

5.5 请求缓存

5.5.1 请求缓存的介绍

1、请求缓存的声明周期是一次请求

2、请求缓存是缓存当前线程中的一个方法,将方法参数作为key,方法的返回结果作为value

3、在一次请求中,目标方法被调用过一次,以后就都会被缓存。

image-20201128123113737

5.5.2 请求缓存的实现

1、创建一个Service,在Service中调用Search服务。

@Service
public class CustomerService {

    @Autowired
    SearchClient searchClient;

    @CacheResult
    @HystrixCommand(commandKey = "findById")
    public Customer findById(Integer id) {
        return searchClient.findById(id);
    }

    @CacheRemove(commandKey = "findById")
    @HystrixCommand
    public void clearFindById(@CacheKey Integer id) {
        System.out.println("findById被清空...");
    }
}

2、使用请求缓存注解

@CacheResult:帮助我们缓存当前方法的返回结果(必须与@HystrixCommand配合使用)
@CacheRemove:帮助我们清除某一个缓存信息(基于commandKey)
@CacheKey:指定哪个方法参数作为缓存的标识

3、编写Search模块返回结果

return new Customer(1, "张三", (int) (Math.random()*10000));

4、编写Filter,去构建HystixRequestContext

@WebFilter("/*")
public class HystrixRequestContextInitFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HystrixRequestContext.initializeContext();
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

5、修改Controller

public Customer findById(@PathVariable Integer id) throws InterruptedException {
    System.out.println(customerService.findById(id).getAge());
    System.out.println(customerService.findById(id).getAge());
    System.out.println(customerService.findById(id));
    System.out.println(customerService.findById(id));
    customerService.clearFindById(id);
    System.out.println(customerService.findById(id));
    System.out.println(customerService.findById(id));
    System.out.println(customerService.findById(id).getAge());
    System.out.println(customerService.findById(id).getAge());
    return customerService.findById(id);
}

6、测试

image-20201128124613887

六.服务的网关-Zuul

6.1 引言

解决的问题:

1、客户端维护大量的ip和port 信息,直接访问指定服务

2、认证和授权操作,需要在每一个模块中都添加认证和授权操作

3、项目的迭代,服务要拆分,服务要合并,需要客户端进行大量的变化

4、统一的把安全性校验放到zuul中

image-20201129110248946

6.2 Zuul的快速入门

1、创建Maven工程,修改为SpringBoot工程

2、导入依赖

<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>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3、添加注解

eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

spring:
  application:
    name: ZUUL
server:
  port: 80

4、测试

image-20201129115016528

6.3 Zuul常用配置信息

6.3.1 Zuul的监控界面

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2、编写配置文件

#查看zuul的监控页面(开发时使用,上线时不要配制)
management:
  endpoints:
    web:
      exposure:
        include: "*"

3、直接访问

image-20201129115904370

6.3.2 忽略服务配置

#zuul配置
zuul:
  # 基于服务名称忽略服务,无法查看,如果需要忽略全部服务:“*” 默认配置的全部路径都会被忽略掉(自定义服务方式使用这种方式无法忽略)
  ignored-services: eureka
  # 监控界面已经可以查看,在访问服务时候,404
  ignored-patterns: /**/search/**

6.3.3 自定义服务配置

#zuul配置
zuul:
  # 指定自定义服务(方式一,key(服务名称):value(路径))
#  routes:
#    search: /ss/**
#    customer: /cc/**
  # 指定自定义服务(方式二)
  routes:
    kehu:   # 自定义名称
      path: /ccc/**   # 映射的路径
      serviceId: customer #服务名称

6.3.4 灰度发布

1、添加配置类

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
    return new PatternServiceRouteMapper(
        "(?<name>^.+)-(?<version>v.+$)",
        "${version}/${name}");
}

2、准备一个服务,提供2个版本

version: v1
spring:
  application:
    name: CUSTOMER-${version}

3、修改Zuul的配置

zuul:
  # 基于服务名称忽略服务,无法查看(如果使用-v的方式,一定忽略掉)
#  ignored-services: "*"

4、测试

image-20201129123840869

6.4 Zuul的过滤器执行流程

客户端发送请求到zuul服务上,首先通过PreFilter链,会把请求转发到RoutingFilter,请求转发到一个指定的服务,在指定的服务响应一个结果之后,再次走一个PostFilter的过滤器链,最终再将响应信息交给客户端

image-20201129132920658

6.5 Zuul过滤器入门

1、创建POJO类,继承ZuulFilter抽象类

@Component
public class TestZuulFilter extends ZuulFilter{}

2、指定当前过滤器的类型

@Override
public String filterType() {
	return FilterConstants.PRE_TYPE;
}

3、指定过滤器的执行顺序

@Override
public int filterOrder() {
    return FilterConstants.PRE_DECORATION_FILTER_ORDER-1;
}

4、配置是否启用

@Override
public boolean shouldFilter() {
    // 开启当前过滤器
    return true;
}

5、测试

image-20201129135624129

6.6 PreFilter实现token校验

1、 准备访问路径,请求参数传递token

http://localhost/v2/customer/version?token=123

2、 创建AuthenticationFilter

@Component
public class AuthenticationFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 1. 获取Request对象
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        // 2. 获取token参数
        String token = request.getParameter("token");
        // 3. 对比token
        if (token == null || !"123".equals(token)) {
            // 4. token校验失败,直接响应数据
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
        }
        return null;
    }
}

3、 在run中编写具体的逻辑代码

@Override
public Object run() throws ZuulException {
    // 1. 获取Request对象
    RequestContext requestContext = RequestContext.getCurrentContext();
    HttpServletRequest request = requestContext.getRequest();
    // 2. 获取token参数
    String token = request.getParameter("token");
    // 3. 对比token
    if (token == null || !"123".equals(token)) {
        // 4. token校验失败,直接响应数据
        requestContext.setSendZuulResponse(false);
        requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
    }
    return null;
}

4、 测试

image-20201129141639584

6.7 Zuul的服务降级

1、 创建POJO类,实现接口FallbackProvider

@Component
public class ZuulFallBack implements FallbackProvider {}

2、 重写两个方法

@Override
public String getRoute() {
    return "*"; // 代表指定全部出现问题的服务,都走这个降级方法
}

@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    // 可以在这里处理日志
    System.out.println("降级的服务:" + route);
    cause.printStackTrace();
    return new ClientHttpResponse() {
        @Override
        public HttpStatus getStatusCode() throws IOException {
            // 指定具体的HttpStatus
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }

        @Override
        public int getRawStatusCode() throws IOException {
            // 返回状态码
            return HttpStatus.INTERNAL_SERVER_ERROR.value();
        }

        @Override
        public String getStatusText() throws IOException {
            // 指定错误信息
            return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();
        }

        @Override
        public void close() {

        }

        @Override
        public InputStream getBody() throws IOException {
            String msg = "当前服务:" + route + "出现了问题!!!";
            return new ByteArrayInputStream(msg.getBytes());
        }

        @Override
        public HttpHeaders getHeaders() {
            // 指定响应头信息
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            return headers;
        }
    };
}

3、 测试

image-20201129144435997

6.8 Zuul动态路由

1、 创建一个过滤器

@Component
public class DynamicRoutingFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 执行顺序最好放在Pre过滤器的最后面
        return FilterConstants.PRE_DECORATION_FILTER_ORDER + 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 1.获取Request
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        // 2.获取参数,redisKey
        String redisKey = request.getParameter("redisKey");

        // 3.直接判断
        if(redisKey != null && redisKey.equalsIgnoreCase("customer")){
            // http://localhost:8080/customer/1
            context.put(FilterConstants.SERVICE_ID_KEY, "customer-v1");
            context.put(FilterConstants.REQUEST_URI_KEY, "/customer/1");

        }else if(redisKey != null && redisKey.equalsIgnoreCase("search")){
            // http://localhost:8081/search/2
            context.put(FilterConstants.SERVICE_ID_KEY, "search");
            context.put(FilterConstants.REQUEST_URI_KEY, "/search/2");
        }

        return null;
    }
}

2、 在run方法中编写业务逻辑

@Override
public Object run() throws ZuulException {
    // 1.获取Request
    RequestContext context = RequestContext.getCurrentContext();
    HttpServletRequest request = context.getRequest();
    // 2.获取参数,redisKey
    String redisKey = request.getParameter("redisKey");

    // 3.直接判断
    if(redisKey != null && redisKey.equalsIgnoreCase("customer")){
        // http://localhost:8080/customer/1
        context.put(FilterConstants.SERVICE_ID_KEY, "customer-v1");
        context.put(FilterConstants.REQUEST_URI_KEY, "/customer/1");

    }else if(redisKey != null && redisKey.equalsIgnoreCase("search")){
        // http://localhost:8081/search/2
        context.put(FilterConstants.SERVICE_ID_KEY, "search");
        context.put(FilterConstants.REQUEST_URI_KEY, "/search/2");
    }

    return null;
}

3、 测试

image-20201129150914394

七. 多语言支持-Sidecar

7.1 引言

在SpringCloud项目中,需要接入一些非java的程序,第三方接口,无法接入Eureka,hystrix,feign等等组件。启动一个代理微服务,代理微服务去和非Java的程序或第三方接口交流,通过代理的微服务去接入SpringCloud的相关组件。

image-20201130095036714

7.2 Sidecar 实现

1、创建第三方的服务

创建一个SpringBoot工程,并且添加一个Controller

2、创建Maven工程(06-sidecar),修改为SpringBoot工程

3、导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-eureka-client</artifactId>
</dependency>

4、添加注解

@EnableSidecar

5、编写配置文件

server:
  port: 81

eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

#指定服务名称
spring:
  application:
    name: oter-service

# 指定代理的第三方服务
sidecar:
  port: 7001

6、通过customer通过feign调用第三方服务

image-20201130115338256

八.服务的消息传递-Stream

8.1 引言

Stream就是在消息队列的基础上,对其进行封装,让我们更方便去操作MQ消息队列(Stream只支持Kafka和RabbitMQ)

SpringCloud架构图-Stream

8.2 Stream快速入门

1、启动RabbitMQ

2、消费者-导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

3、消费者-配置文件

spring:
  # 连接RabbitMQ
  rabbitmq:
    host: 192.168.75.139
    port: 5672
    username: test
    password: test
    virtual-host: /test

4、消费者-监听队列

public interface StreamClient {
    @Input("myMessage")
    SubscribableChannel input();
}
//------------------------------------
@Component
@EnableBinding(StreamClient.class)
public class StreamReceiver {
    @StreamListener("myMessage")
    public void msg(Object msg) {
        System.out.println("接收到消息:" + msg);
    }
}

5、生产者-导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

6、生产者-配置文件

spring:
  # 连接RabbitMQ
  rabbitmq:
    host: 192.168.75.139
    port: 5672
    username: test
    password: test
    virtual-host: /test

7、生产者-发布消息

public interface StreamClient {
    @Output("myMessage")
    MessageChannel output();
}
//---------------------------------------
// 在启动类中添加注解
@EnableBinding(StreamClient.class)
//---------------------------------------
@RestController
public class MessageController {
    @Autowired
    private StreamClient streamClient;

    @GetMapping("/send")
    public String send() {
        String msg = "Hello Stream!!!";
        boolean send = streamClient.output().send(MessageBuilder.withPayload(msg).build());
        return "消息发送成功!!!";
    }
}

8.3 Stream重复消费问题

只需要添加一个配置,指定消费者组

spring:
  cloud:
    stream:
        bindings:
          myMessage: 		# 队列名称
            group: customer # 消费者组

8.4 Stream的消费者手动ack

1、编写配置

spring:
  cloud:
    stream:
        rabbit:
          bindings:
            myMessage:
              consumer:
                acknowledgeMode: MANUAL

2、修改消费端方法

@StreamListener("myMessage")
public void msg(Object msg,
                @Header(name = AmqpHeaders.CHANNEL) Channel channel,
                @Header(name = AmqpHeaders.DELIVERY_TAG) Long deliveryTag) throws IOException {
    System.out.println("接收到消息:" + msg);
    channel.basicAck(deliveryTag, false);
}

九.服务的动态配置-Config

9.1 引言

1、配置文件分散不同的项目中,不方便维护

2、配置文件的安全性问题

3、修改完配置文件,无法立即生效

image-20201211095940150

9.2 搭建Config-Server

1、创建Maven工程,修改为SpringBoot

2、导入依赖

<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>

3、添加注解

@SpringBootApplication
@EnableConfigServer

4、编写配置文件(Git的操作)

spring:
  application:
    name: CONFIG
  cloud:
    config:
      server:
        git:
          basedir: D:\basedir # 本地仓库地址
          username: jinshengwang
          password: jin6150090
          uri: https://gitee.com/jinshengwang/config-resp.git

5、测试(http://localhost:port/{label}/{application}-{profile}.yml)

image-20201211114015211

9.3 修改Customer连接Config

1、导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

2、编写配置文件

eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

spring:
  application:
    name: CUSTOMER-${version}
  cloud:
    config:
      discovery:
        enabled: true
        service-id: CONFIG
      profile: dev

version: v1

# CONFIG - CUSTOMER-v1-dev.yml

3、修改配置名称

修改为bootstrap.yml

4、测试发布消息到RabbitMQ(http://localhost:8080/send)

image-20201211123408318

9.4 实现动态配置

9.4.1 实现原理

image-20201211133824649

9.4.2 服务连接RabbitMQ

1、导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

2、编写配置文件连接RabbitMQ信息

spring:
  rabbitmq:
    host: 192.168.75.139
    port: 5672
    username: test
    password: test
    virtual-host: /test

9.4.3 实现手动刷新

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2、编写配置文件

management:
  endpoints:
    web:
      exposure:
        include: "*"

3、为customer添加一个controller

@RefreshScope
public class CustomerController {
    @Value("${env}")
    private String env;
    
    @GetMapping("/env")
    public String env(){
        return env;
    }
}

4、测试

1. CONFIG在Gitee修改后,自动拉取最新的配置信息。
2. 其他模块需要更新的话,手动发送一个请求http://ip:10000/actuator/bus-refresh,不重启项目,就可以获取最新的配置信息。

9.4.4 内网穿透

1、内网穿透软件官网:natapp.cn

2、注册登录

3、购买个免费的通道

image-20201211150446971

4、下载客户端,创建config.ini文件,复制内存,并且设置authtoken值

image-20201211150649956

5、启动exe文件,并用测试域名访问config接口

image-20201211150621299

9.4.5 实现自动刷新

1、配置Gitee中的WebHooks

image-20201211151311254

2、给Config添加一个过滤器

添加WebHooksFilter过滤器,解决Gitlab webhook触发配置中心/actuator/bus-refresh请求返回400错误码的问题

3、测试

image-20201211152607804

十.服务的追踪-Sleuth

10.1 引言

在整个微服务架构中,微服务很多,一个请求可能需要调用多个微服务,才能最终完成一个功能,如果在这过程中发生问题,这么多服务是问题点在哪,问题的原因是什么。

1、Sleuth可以获取整个服务的链路信息。

2、整合Zipkin通过图形化界面查看信息

3、Sleuth可以把日志存储在数据库中。

image-20201211155802646

10.2 Sleuth的使用

1、导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

2、编写配置文件

logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: DEBUG

3、测试

image-20201211161839883

SEARH:服务名称
36e7...:总链路id
eca8...:当前服务的链路id
true:日志会持久化输出,如果是false:不会把当前的日志信息输出其他系统中。

10.3 Zipkin的使用

1、搭建Zipkin的web工程,官网:zipkin.io/

version: '3.1'
services:
  zipkin:
    image: openzipkin/zipkin:2.12
    restart: always
    container_name: zipkin
    ports:
      - 9411:9411

2、导入依赖,需要替换原有的spring-cloud-starter-sleuth,因为spring-cloud-starter-zipkin中包含了sleuth包了

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

3、编写配置文件

spring:
  sleuth:
    sampler:
      probability: 1 # 百分之多少sleuth信息输出到zipkin中
  zipkin:
    base-url: http://192.168.75.139:9411/

4、测试

image-20201211182646240

10.4 整合RabbitMQ

1、导入RabbitMQ依赖

spring-cloud-starter-zipkin中自带RabbitMQ的依赖

image-20201212141250023

2、修改配置文件

spring:
  zipkin:
    base-url: http://192.168.75.139:9411/
    sender:
      type: rabbit

3、修改Zipkin的信息

version: '3.1'
services:
  zipkin:
    image: openzipkin/zipkin
    restart: always
    container_name: zipkin
    ports:
      - 9411:9411
    environment:
      - RABBIT_ADDRESSES=192.168.75.139
      - RABBIT_PASSWORD=test
      - RABBIT_USER=test
      - RABBIT_VIRTUAL_HOST=/test

4、测试

image-20201212141406837

10.5 Zipkin存储数据到ES

1、重新修改Zipkin的yml文件

version: '3.1'
services:
  zipkin:
    image: openzipkin/zipkin
    restart: always
    container_name: zipkin
    ports:
      - 9411:9411
    environment:
      - RABBIT_ADDRESSES=192.168.75.139
      - RABBIT_PASSWORD=test
      - RABBIT_USER=test
      - RABBIT_VIRTUAL_HOST=/test
      - STORAGE_TYPE=elasticsearch
      - ES_HOSTS=http://192.168.75.139:9200

注意:zipkin不能是最新版本的(我回退老版本了openzipkin/zipkin:2.12),不然和ES6.5.4不兼容。

十一.完整的SpringCloud架构图

image-20201212161225707

十二.填坑

12.1 Eureka节点不同步

启动Eureka时,必须一个一个启动,启动Eureka1执行完后,在启动Eureka2,不然节点信息没有同步,不理解待研究...

12.2 Zipkin整合ES

起先Zipkin我安装的是最新版本,ES是6.5.4,但是Zipkin就是不能连接ES。我把Zipkin回退到2.12版本,就可以集成了。