Spring Cloud

Spring Cloud:[klaʊd]
Spring Cloud是什么?
Spring Cloud 是一系列框架的有序集合。它利用 Spring Boot 的开发便利性,巧妙地简化了分布式系统基础设施的开发,如服务注册、服务发现、配置中心、消息总线、负载均衡、断路器、数据监控等,这些都可以用 Spring Boot 的开发风格做到一键启动和部署。
通俗地讲,Spring Cloud 就是用于构建微服务开发和治理的框架集合(并不是具体的一个框架),主要贡献来自 Netflix OSS。
Spring Cloud 模块介绍
Spring Cloud 模块的相关介绍如下:
Eureka [juˈriːkə] :服务注册中心,用于服务管理。
Ribbon [ˈrɪbən] :基于客户端的负载均衡组件。
Hystrix:容错框架,能够防止服务的雪崩效应。
Feign [feɪn]:Web 服务客户端,能够简化 HTTP 接口的调用。
Zuul:API 网关,提供路由转发、请求过滤等功能。
Config:分布式配置管理。
Sleuth [sluːθ]:服务跟踪。
Stream:构建消息驱动的微服务应用程序的框架。
Bus:消息代理的集群消息总线。
除了上述模块,还有 Cli、Task等。教程中只介绍一些常用的模块。
Spring Cloud 是一个非常好的框架集合,它包含的功能模块非常多,不可能一一讲解到
Spring Cloud版本介绍
在第一次访问 Spring Cloud 官网时一定会有一个疑惑那就是版本太多了,到底哪个是稳定版本?哪个才是自己需要的版本?接下来就给大家简单介绍一下版本的问题。
访问官网 https://projects.spring.io/spring-cloud/#learn 可以看到网页右侧的版本列表
Spring Cloud_第1张图片

从图中可以看到 Spring Cloud 不是像别的项目那样,版本号采用 1.1、1.2、1.3 这种的格式。因为 Spring Cloud 是一个拥有诸多子项目的大型综合项目,可以说是对微服务架构解决方案的综合套件组件,其中包含的各个子项目都独立进行着内容的迭代与更新,各自维护着自己的发布版本号。

至于怎么选择适合自己的版本,笔者认为,大家可以在接触的时候直接选最新的稳定版本。新版本中的 Bug 肯定要少,并且更稳定。

本课件的案例都是基于Hoxton.SR9 CURRENT GA进行讲解的。不同的版本有不同的功能,对应的每个子模块的版本也不一样,那么如何知道每个大版本下面具体的子模块是什么版本呢?
答案就在官网的首页上面,在页面的最下方有一个表格,通过这个表格我们可以清楚地知道 Hoxton.SR9 CURRENT GA对应的 Spring Boot 版本是Supported Boot Version: 2.3.5.RELEASE,Spring-Cloud-Bus 是 spring-cloud-bus Reference Documentation, version 2.2.3.RELEASE。
参考文件由以下部分组成:
Documentation Overview About the Documentation, Getting Help, First Steps, and more.
spring-cloud-aws spring-cloud-aws Reference Documentation, version 2.2.5.RELEASE
spring-cloud-build spring-cloud-build Reference Documentation, version 2.3.1.RELEASE
spring-cloud-bus spring-cloud-bus Reference Documentation, version 2.2.3.RELEASE
spring-cloud-circuitbreaker spring-cloud-circuitbreaker Reference Documentation, version 1.0.4.RELEASE
spring-cloud-cli spring-cloud-cli Reference Documentation, version 2.2.3.RELEASE
spring-cloud-cloudfoundry spring-cloud-cloudfoundry Reference Documentation, version 2.2.3.RELEASE
spring-cloud-commons spring-cloud-commons Reference Documentation, version 2.2.6.RELEASE
spring-cloud-config spring-cloud-config Reference Documentation, version 2.2.6.RELEASE
spring-cloud-consul spring-cloud-consul Reference Documentation, version 2.2.5.RELEASE
spring-cloud-contract spring-cloud-contract Reference Documentation, version 2.2.5.RELEASE
spring-cloud-function spring-cloud-function Reference Documentation, version 3.0.11.RELEASE
spring-cloud-gateway spring-cloud-gateway Reference Documentation, version 2.2.6.RELEASE
spring-cloud-gcp spring-cloud-gcp Reference Documentation, version 1.2.6.RELEASE
spring-cloud-kubernetes spring-cloud-kubernetes Reference Documentation, version 1.1.7.RELEASE
spring-cloud-netflix spring-cloud-netflix Reference Documentation, version 2.2.6.RELEASE
spring-cloud-openfeign spring-cloud-openfeign Reference Documentation, version 2.2.6.RELEASE
spring-cloud-security spring-cloud-security Reference Documentation, version 2.2.4.RELEASE
spring-cloud-sleuth spring-cloud-sleuth Reference Documentation, version 2.2.6.RELEASE
spring-cloud-task spring-cloud-task Reference Documentation, version 2.2.3.RELEASE
spring-cloud-vault spring-cloud-vault Reference Documentation, version 2.2.6.RELEASE
spring-cloud-zookeeper spring-cloud-zookeeper Reference Documentation, version 2.2.4.RELEASE
Spring Cloud Eureka是什么?
Spring Cloud Eureka 是 Spring Cloud Netflix 微服务套件的一部分,基于 Netflix Eureka 做了二次封装,主要负责实现微服务架构中的服务治理功能。
Spring Cloud Eureka 是一个基于 REST 的服务,并且提供了基于 Java 的客户端组件,能够非常方便地将服务注册到 Spring Cloud Eureka 中进行统一管理。
服务治理是微服务架构中必不可少的一部分,阿里开源的 Dubbo 框架就是针对服务治理的。服务治理必须要有一个注册中心,除了用 Eureka 作为注册中心外,我们还可以使用 Consul、Etcd、Zookeeper 等来作为服务的注册中心。
用过 Dubbo 的读者应该清楚,Dubbo 中也有几种注册中心,比如基于 Zookeeper、基于 Redis 等,不过用得最多的还是 Zookeeper 方式。
至于使用哪种方式都是可以的,注册中心无非就是管理所有服务的信息和状态。若用我们生活中的例子来说明的话,觉得 12306 网站比较合适。
首先,12306 网站就好比一个注册中心,顾客就好比调用的客户端,当他们需要坐火车时,就会登录 12306 网站上查询余票,有票就可以购买,然后获取火车的车次、时间等,最后出发。
程序也是一样,当你需要调用某一个服务的时候,你会先去 Eureka 中去拉取服务列表,查看你调用的服务在不在其中,在的话就拿到服务地址、端口等信息,然后调用。

注册中心带来的好处就是,不需要知道有多少提供方,你只需要关注注册中心即可,就像顾客不必关心有多少火车在开行,只需要去 12306 网站上看有没有票就可以了。
为什么 Eureka 比 Zookeeper 更适合作为注册中心呢?主要是因为 Eureka 是基于 AP 原则构建的,而 ZooKeeper 是基于 CP 原则构建的。
在分布式系统领域有个著名的 CAP 定理,即 C 为数据一致性;A 为服务可用性;P 为服务对网络分区故障的容错性。这三个特性在任何分布式系统中都不能同时满足,最多同时满足两个。
Zookeeper 有一个 Leader,而且在这个 Leader 无法使用的时候通过 Paxos(ZAB)算法选举出一个新的 Leader。这个 Leader 的任务就是保证写数据的时候只向这个 Leader 写入,Leader 会同步信息到其他节点。通过这个操作就可以保证数据的一致性。
总而言之,想要保证 AP 就要用 Eureka,想要保证 CP 就要用 Zookeeper。
Dubbo 中大部分都是基于 Zookeeper 作为注册中心的。Spring Cloud 中当然首选 Eureka。

SpringCloud组件:搭建Eureka服务注册中心
首先创建一个 Maven 项目,取名为 eureka-server,在 pom.xml 中配置 Eureka 的依赖信息,代码如下所示

<!-- Spring Boot -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.6.RELEASE</version>
    <relativePath />
</parent>
<dependencies>
    <!-- eureka -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>
<!-- Spring Cloud -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.SR2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

创建一个启动类 EurekaServerApplication,代码如下所示。

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

这里所说的启动类,跟我们之前讲的 Spring Boot 几乎完全一样,只是多了一个 @EnableEurekaServer 注解,表示开启 Eureka Server。
接下来在 src/main/resources 下面创建一个 application.properties 属性文件,增加下面的配置:

spring.application.name=eureka-server
server.port=8761

由于该应用为注册中心, 所以设置为false, 代表不向注册中心注册自己
eureka.client.register-with-eureka=false
#由于注册中心的职责就是维护服务实例, 它并不需要去检索服务, 所以也设置为 false
eureka.client.fetch-registry=false
eureka.client.register-with-eureka 一定要配置为 false,不然启动时会把自己当作客户端向自己注册,会报错。
接下来直接运行 EurekaServerApplication 就可以启动我们的注册中心服务了。我们在 application.properties 配置的端口是 8761,则可以直接通过 http://localhost:8761/ (http://localhost%EF%BC%9A8761/) 去浏览器中访问,然后便会看到 Eureka 提供的 Web 控制台。
Spring Cloud_第2张图片

使用Eureka编写服务提供者
1)创建项目注册到 Eureka
注册中心已经创建并且启动好了,接下来我们实现将一个服务提供者 eureka-client-user-service 注册到 Eureka 中,并提供一个接口给其他服务调用。
首先还是创建一个 Maven 项目,然后在 pom.xml 中增加相关依赖,代码如下所示。

<!-- Spring Boot -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.6.RELEASE</version>
    <relativePath />
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- eureka -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

<!-- Spring Cloud -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.SR2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

创建一个启动类 App,代码如下所示。

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

启动类的方法与之前没有多大区别,只是注解换成 @EnableDiscoveryClient,表示当前服务是一个 Eureka 的客户端。
接下来在 src/main/resources 下面创建一个 application.properties 属性文件,增加下面的配置:

spring.application.name= eureka-client-user-service
server.port=8081
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

采用IP注册

eureka.instance.preferIpAddress=true

定义实例ID格式

eureka.instance.instance-id= s p r i n g . a p p l i c a t i o n . n a m e : {spring.application.name}: spring.application.name:{spring.cloud.client.ip-address}{server.port}
eureka.client.serviceUrl.defaultZone 的地址就是我们之前启动的 Eureka 服务的地址,在启动的时候需要将自身的信息注册到 Eureka 中去。
执行 App 启动服务,我们可以看到控制台中有输出注册信息的日志:
DiscoveryClient_EUREKA-CLIENT-USER-SERVICE/eureka-client-user-service:192.168.31.245:8081 - registration status: 204
我们可以进一步检查服务是否注册成功。回到之前打开的 Eureka 的 Web 控制台,刷新页面,就可以看到新注册的服务信息了。
Spring Cloud_第3张图片

2)编写提供接口
创建一个 Controller,提供一个接口给其他服务查询,代码如下所示。

@RestController
public class UserController {
    @GetMapping("/user/hello")
    public String hello() {
        return “hello”;
    }
}

重启服务,访问 http://localhost:8081/user/hello (http://localhost%EF%BC%9A8081/user/hello),如果能看到我们返回的 Hello 字符串,就证明接口提供成功了。
Spring Cloud_第4张图片

使用Eureka编写服务消费者
1)直接调用接口
创建服务消费者,消费我们刚刚编写的 user/hello 接口,同样需要先创建一个 Maven 项目 eureka-client-article-service,然后添加依赖,依赖和服务提供者的一样,这里就不贴代码了。
创建启动类 App,启动代码与前面所讲也是一样的。唯一不同的就是 application.properties 文件中的配置信息:
spring.application.name=eureka-client-article-service
server.port=8082

1)通过 RestTemplate 来消费接口
RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。我们通过配置 RestTemplate 来调用接口,代码如下所示。

@Configuration
public class BeanConfiguration {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

创建接口,在接口中调用 user/hello 接口,代码如下所示。

@RestController
public class ArticleController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/article /callHello")
    public String callHello() {
        return restTemplate.getForObject("http://localhost:8081/user/hello", String.class);
    }
}

执行 App 启动消费者服务,访问 /article/callHello 接口来看看有没有返回 Hello 字符串,如果返回了就证明调用成功。访问地址为 http://localhost:8082/article/callHello (http://localhost%EF%BC%9A8082/article/callHello)。
2)通过 Eureka 来消费接口[常用]
上面提到的方法是直接通过服务接口的地址来调用的,和我们之前的做法一样,完全没有用到 Eureka 带给我们的便利。既然用了注册中心,那么客户端调用的时候肯定是不需要关心有多少个服务提供接口,下面我们来改造之前的调用代码。
首先改造 RestTemplate 的配置,添加一个 @LoadBalanced 注解,这个注解会自动构造 LoadBalancerClient 接口的实现类并注册到 Spring 容器中,代码如下所示。

@Configuration
public class BeanConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

接下来就是改造调用代码,我们不再直接写固定地址,而是写成服务的名称,这个名称就是我们注册到 Eureka 中的名称,是属性文件中的 spring.application.name,相关代码如下所示。

@GetMapping("/article/callHello2")
public String callHello2() {
    return restTemplate.getForObject("http://eureka-client-user-service/user/hello", String.class);
}

Spring Cloud Ribbon(负载均衡器)介绍及使用
目前主流的负载方案分为以下两种:
1.集中式负载均衡,在消费者和服务提供方中间使用独立的代理方式进行负载,有硬件的(比如 F5),也有软件的(比如 Nginx)。
2.客户端自己做负载均衡,根据自己的请求情况做负载,Ribbon 就属于客户端自己做负载。
Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。
Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的(https://github.com/Netflix/ribbon)。
Ribbon 模块
Spring Cloud_第5张图片

Ribbon 使用
我们使用 Ribbon 来实现一个最简单的负载均衡调用功能,接口就用使用Eureka编写服务提供者提供的 /user/hello 接口,需要启动两个服务,一个是 8081 的端口,一个是 8083 的端口。
然后创建一个新的 Maven 项目 ribbon-native-demo,在项目中集成 Ribbon,在 pom.xml 中添加如下代码所示的依赖。

<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-core</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.ribbon</groupId>
    <artifactId>ribbon-loadbalancer</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>io.reactivex</groupId>
    <artifactId>rxjava</artifactId>
    <version>1.0.10</version>
</dependency>

接下来我们编写一个客户端来调用接口,代码如下所示。

// 服务列表
List<Server> serverList = Lists.newArrayList(new Server("localhost", 8081), new Server("localhost", 8083));
// 构建负载实例
ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
// 调用 5 次来测试效果
for (int i = 0; i < 5; i++) {
    String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build()
            .submit(new ServerOperation<String>() {
                public Observable<String> call(Server server) {
                    try {
                        String addr = "http://" + server.getHost() + ":" + server.getPort() + "/user/hello";
                        System.out.println(" 调用地址:" + addr);
                        URL url = new URL(addr);
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        conn.setRequestMethod("GET");
                        conn.connect();
                        InputStream in = conn.getInputStream();
                        byte[] data = new byte[in.available()];
                        in.read(data);
                        return Observable.just(new String(data));
                    } catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            }).toBlocking().first();
    System.out.println(" 调用结果:" + result);
}

上述这个例子主要演示了 Ribbon 如何去做负载操作,调用接口用的最底层的 HttpURLConnection。当然你也可以用别的客户端,或者直接用 RibbonClient 执行程序,可以看到控制台输出的结果如下:
Spring Cloud_第6张图片

Spring Cloud Ribbon结合RestTemplate实现负载均衡
在上一节内容中我们简单地使用 Ribbon 进行了负载的一个调用,这意味着 Ribbon 是可以单独使用的。
在 Spring Cloud 中使用Ribbon会更简单,因为Spring Cloud在Ribbon的基础上进行了一层封装,将很多配置都集成好了。本节将在 Spring Cloud 项目中使用 Ribbon。
使用 RestTemplate 与整合 Ribbon
Spring 提供了一种简单便捷的模板类来进行 API 的调用,那就是RestTemplate。

  1. 使用RestTemplate
    在前面介绍 Eureka 时,我们已经使用过 RestTemplate 了,本节会更加详细地跟大家讲解 RestTemplate 的具体使用方法。
    首先我们来看看 GET 请求的使用方式:创建一个新的项目 spring-rest-template,配置好 RestTemplate:
@Configuration
public class BeanConfiguration {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

新建一个 HouseController,并增加两个接口,一个通过 @RequestParam 来传递参数,返回一个对象信息;另一个通过 @PathVariable 来传递参数,返回一个字符串。请尽量通过两个接口组装不同的形式,具体代码如下所示。

@GetMapping("/house/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return new HouseInfo(1L, "西安" "雁塔" "长安小区");
}

@GetMapping("/house/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return name;
}

新建一个 HouseClientController 用于测试,使用 RestTemplate 来调用我们刚刚定义的两个接口,代码如下所示。

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data?name="+ name, HouseInfo.class);
}

@GetMapping("/call/data/{name}")
public String getData2(@PathVariable("name") String name) {
    return restTemplate.getForObject( "http://localhost:8081/house/data/{name}", String.class, name);
}

获取数据结果可通过RestTemplate的getForObject方法(如下代码所示)来实现,此方法有三个重载的实现:
url:请求的 API 地址,有两种方式,其中一种是字符串,另一种是 URI 形式。
responseType:返回值的类型。
uriVariables:PathVariable参数,有两种方式,其中一种是可变参数,另一种是 Map 形式。

public <T> T getForObject(String url, Class<T> responseType,Object... uriVariables);
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables);
public <T> T getForObject(URI url, Class<T> responseType);

除了 getForObject,我们还可以使用 getForEntity 来获取数据,代码如下所示。

@GetMapping("/call/dataEntity")
public HouseInfo getData(@RequestParam("name") String name) {
    ResponseEntity<HouseInfo> responseEntity = restTemplate
            .getForEntity("http://localhost:8081/house/data?name=" + name, HouseInfo.class);
    if (responseEntity.getStatusCodeValue() == 200) {
        return responseEntity.getBody();
    }
    return null;
}

getForEntity 中可以获取返回的状态码、请求头等信息,通过 getBody 获取响应的内容。其余的和 getForObject 一样,也是有 3 个重载的实现。
接下来看看怎么使用 POST 方式调用接口。在 HouseController 中增加一个 save 方法用来接收 HouseInfo 数据,代码如下所示。

@PostMapping("/house/save")
public Long addData(@RequestBody HouseInfo houseInfo) {
    System.out.println(houseInfo.getName());
    return 1001L;
}

接着写调用代码,用 postForObject 来调用,代码如下所示。

@GetMapping("/call/save")
public Long add() {
    HouseInfo houseInfo = new HouseInfo();
    houseInfo.setCity("西安");
    houseInfo.setRegion("雁塔");
    houseInfo.setName("×××");
    Long id = restTemplate.postForObject("http://localhost:8081/house/save", houseInfo, Long.class);
    return id;
}

postForObject 同样有 3 个重载的实现。除了 postForObject 还可以使用 postForEntity 方法,用法都一样,代码如下所示。

public <T> T postForObject(String url, Object request,Class<T> responseType, Object... uriVariables);
public <T> T postForObject(String url, Object request,Class<T> responseType, Map<String, ?> uriVariables);
public <T> T postForObject(URI url, Object request, Class<T> responseType);

除了 get 和 post 对应的方法之外,RestTemplate 还提供了 put、delete 等操作方法,还有一个比较实用的就是 exchange 方法。exchange 可以执行 get、post、put、delete 这 4 种请求方式。更多地使用方式大家可以自行学习。
2. 整合 Ribbon
在 Spring Cloud 项目中集成 Ribbon 只需要在 pom.xml 中加入下面的依赖即可,其实也可以不用配置,因为 Eureka 中已经引用了Ribbon,代码如下所示。

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

RestTemplate 负载均衡示例
前面我们调用接口都是通过具体的接口地址来进行调用,RestTemplate 可以结合 Eureka 来动态发现服务并进行负载均衡的调用。
修改RestTemplate 的配置,增加能够让 RestTemplate 具备负载均衡能力的注解 @LoadBalanced。代码如下所示。

@Configuration
public class BeanConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

修改接口调用的代码,将 ip+port 改成服务名称,也就是注册到 Eureka 中的名称,代码如下所示。

@GetMapping("/call/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForObject("http://ribbon-eureka-demo/house/data?name=" + name, HouseInfo.class);
}

接口调用的时候,框架内部会将服务名称替换成具体的服务 IP 信息,然后进行调用。
@LoadBalanced 注解原理
相信大家一定有一个疑问:为什么在 RestTemplate 上加了一个 @LoadBalanced 之后,RestTemplate 就能够跟 Eureka 结合了,不但可以使用服务名称去调用接口,还可以负载均衡?
应该归功于 Spring Cloud 给我们做了大量的底层工作,因为它将这些都封装好了,我们用起来才会那么简单。框架就是为了简化代码,提高效率而产生的。
这里主要的逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务地址,然后再去调用,这就是 @LoadBalanced 的原理。
下面我们来实现一个简单的拦截器,看看在调用接口之前会不会进入这个拦截器。我们不做任何操作,就输出一句话,证明能进来就行了。
具体代码如下所示。

public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;
    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }
    public MyLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }
    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        System.out.println("进入自定义的请求拦截器中" + serviceName);
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

拦截器设置好了之后,我们再定义一个注解,并复制 @LoadBalanced 的代码,改个名称就可以了,代码如下所示。

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}

然后定义一个配置类,给 RestTemplate 注入拦截器,代码如下所示。

@Configuration
public class MyLoadBalancerAutoConfiguration {
    @MyLoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();
    @Bean
    public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
        return new MyLoadBalancerInterceptor();
    }
    @Bean
    public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
        return new SmartInitializingSingleton() {
          @Override
          public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : MyLoadBalancerAutoConfiguration.this.restTemplates){
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
                list.add(myLoad BalancerInterceptor());
                restTemplate.setInterceptors(list);
            }
          }
        };
    }
}

维护一个 @MyLoadBalanced 的 RestTemplate 列表,在 SmartInitializingSingleton 中对 RestTemplate 进行拦截器设置。
然后改造我们之前的 RestTemplate 配置,将 @LoadBalanced 改成我们自定义的 @MyLoadBalanced,代码如下所示。

@Bean
//@LoadBalanced
@MyLoadBalanced
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

重启服务,访问服务中的接口就可以看到控制台的输出了,这证明在接口调用的时候会进入该拦截器,输出如下:
进入自定义的请求拦截器中 ribbon-eureka-demo
通过这个小案例我们就能够清楚地知道 @LoadBalanced 的工作原理。接下来我们来看看源码中是怎样的一个逻辑。
首先看配置类,如何为 RestTemplate 设置拦截器,代码在 spring-cloud-commons.jar 中的 org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration 类里面通过查看 LoadBalancerAutoConfiguration 的源码,可以看到这里也是维护了一个 @LoadBalanced 的 RestTemplate 列表,代码如下所示。

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(final List<RestTemplateCustomizer> customizers) {
    return new SmartInitializingSingleton() {
        @Override
        public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        }
    };
}

通过查看拦截器的配置可以知道,拦截器用的是 LoadBalancerInterceptor,RestTemplate Customizer 用来添加拦截器,代码如下所示。

@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
public static class LoadBalancerInterceptorConfig {
    @Bean
    public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient,
            LoadBalancerRequestFactory requestFactory) {
        return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    }
    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
        return new RestTemplateCustomizer() {
            @Override
            public void customize(RestTemplate restTemplate) {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                  restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            }
        };
    }
}

拦截器的代码在 org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor 中,代码如下所示。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname:" + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

主要的逻辑在 intercept 中,执行交给了 LoadBalancerClient 来处理,通过 LoadBalancer RequestFactory 来构建一个 LoadBalancerRequest 对象,代码如下所示。

public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request, final byte[] body,
        final ClientHttpRequestExecution execution) {
    return new LoadBalancerRequest<ClientHttpResponse>() {
        @Override
        public ClientHttpResponse apply(final ServiceInstance instance) throws Exception {
            HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
            if (transformers != null) {
                for (LoadBalancerRequestTransformer transformer : transformers) {
                    serviceRequest = transformer.transformRequest(serviceRequest,instance);
                }
            }
            return execution.execute(serviceRequest, body);
        }
    };
}

createRequest 中通过 ServiceRequestWrapper 来执行替换 URI 的逻辑,ServiceRequest Wrapper 中将 URI 的获取交给了 org.springframework.cloud.client.loadbalancer.LoadBalancer Client#reconstructURI 方法。
以上就是整个 RestTemplate 结合 @LoadBalanced 的执行流程,至于具体的实现大家可以自己去研究,这里只介绍原理及整个流程。
Ribbon API 使用
当你有一些特殊的需求,想通过 Ribbon 获取对应的服务信息时,可以使用 Load-Balancer Client 来获取,比如你想获取一个 ribbon-eureka-demo 服务的服务地址,可以通过 LoadBalancerClient 的 choose 方法来选择一个:

@Autowired
private LoadBalancerClient loadBalancer;

@GetMapping("/choose")
public Object chooseUrl() {
    ServiceInstance instance = loadBalancer.choose("ribbon-eureka-demo");
    return instance;
}
访问接口,可以看到返回的信息如下:
{
    serviceId: "ribbon-eureka-demo",
    server: {
        host: "localhost",
        port: 8081,
        id: "localhost:8081",
        zone: "UNKNOWN",
        readyToServe: true,
        alive: true,
        hostPort: "localhost:8081",
        metaInfo: {
            serverGroup: null,
            serviceIdForDiscovery: null, instanceId: "localhost:8081",
            appName: null
        }
    },
    secure: false, metadata: { }, host: "localhost", port: 8081,
    uri: "http://localhost:8081"
}

Ribbon 饥饿加载
从网上看到很多博客中都提到过的一种情况:在进行服务调用的时候,如果网络情况不好,第一次调用会超时。有很多大神对此提出了解决方案,比如把超时时间改长一点、禁用超时等。
Spring Cloud 目前正在高速发展中,版本更新很快,我们能发现的问题基本上在版本更新的时候就修复了,或者提供最优的解决方案。
超时的问题也是一样,Ribbon 的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化 Client 的时间再加上请求接口的时间,就会导致第一次请求超时。
基于 Finchley.SR2这个版本已经提供了一种针对上述问题的解决方法,那就是 eager-load 方式。通过配置 eager-load 来提前初始化客户端就可以解决这个问题。
ribbon.eager-load.enabled=true //开启 Ribbon 的饥饿加载模式。
//指定需要饥饿加载的服务名,也就是你需要调用的服务,若有多个则用逗号隔开。
ribbon.eager-load.clients=ribbon-eureka-demo
怎么进行验证呢?网络情况确实不太好模拟,不过通过调试源码的方式即可验证,在 org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration 中找到对应的代码,代码如下所示。

@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
    return new RibbonApplicationContextInitializer(springClientFactory(),ribbonEagerLoadProperties.getClients());
}

在 return 这行设置一个断点,然后以调试的模式启动服务,如果能进入到这个断点的代码这里,就证明配置生效了.
Spring Cloud Ribbon负载均衡策略介绍
Ribbon 作为一款客户端负载均衡框架,默认的负载策略是轮询,同时也提供了很多其他的策略,能够让用户根据自身的业务需求进行选择。
整体策略代码实现类如图。

图中说明如下:
1)BestAvailabl
选择一个最小的并发请求的 Server,逐个考察 Server,如果 Server 被标记为错误,则跳过,然后再选择 ActiveRequestCount 中最小的 Server。
2)AvailabilityFilteringRule
过滤掉那些一直连接失败的且被标记为 circuit tripped 的后端 Server,并过滤掉那些高并发的后端 Server 或者使用一个 AvailabilityPredicate 来包含过滤 Server 的逻辑。其实就是检查 Status 里记录的各个 Server 的运行状态。
3)ZoneAvoidanceRule
使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个 Server,前一个判断判定一个 Zone 的运行性能是否可用,剔除不可用的 Zone(的所有 Server),AvailabilityPredicate 用于过滤掉连接数过多的 Server。
4)RandomRule
随机选择一个 Server。
5)RoundRobinRule
轮询选择,轮询 index,选择 index 对应位置的 Server。
6)RetryRule
对选定的负载均衡策略机上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择 Server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 Server。
7)ResponseTimeWeightedRule
作用同 WeightedResponseTimeRule,ResponseTime-Weighted Rule 后来改名为 WeightedResponseTimeRule。
8)WeightedResponseTimeRule
根据响应时间分配一个 Weight(权重),响应时间越长,Weight 越小,被选中的可能性越低。
Spring Cloud Ribbon自定义负载均衡策略
通过实现 IRule 接口可以自定义负载策略,主要的选择服务逻辑在 choose 方法中。我们这边只是演示怎么自定义负载策略,所以没写选择的逻辑,直接返回服务列表中第一个服务。具体代码如下所示。

public class MyRule implements IRule {

    private ILoadBalancer lb;

    @Override
    public Server choose(Object key) {
        List<Server> servers = lb.getAllServers();
        for (Server server : servers) {
            System.out.println(server.getHostPort());
        }
        return servers.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return lb;
    }
}

在 Spring Cloud 中,可通过配置的方式使用自定义的负载策略,ribbon-config-demo 是调用的服务名称。
ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=net.biancheng.ribbon_eureka_demo.rule.MyRule
重启服务,访问调用了其他服务的接口,可以看到控制台的输出信息中已经有了我们自定义策略中输出的服务信息,并且每次都是调用第一个服务。这跟我们的逻辑是相匹配的。
Ribbon 的一些常用配置

  1. 禁用 Eureka
    当我们在 RestTemplate 上添加 @LoadBalanced 注解后,就可以用服务名称来调用接口了,当有多个服务的时候,还能做负载均衡。
    这是因为 Eureka 中的服务信息已经被拉取到了客户端本地,如果我们不想和 Eureka 集成,可以通过下面的配置方法将其禁用。

禁用 Eureka

ribbon.eureka.enabled=false
当我们禁用了 Eureka 之后,就不能使用服务名称去调用接口了,必须指定服务地址。
2. 配置接口地址列表
上面我们讲了可以禁用 Eureka,禁用之后就需要手动配置调用的服务地址了,配置如下:

禁用 Eureka 后手动配置服务地址

ribbon-config-demo.ribbon.listOfServers=localhost:8081,localhost:8083
这个配置是针对具体服务的,前缀就是服务名称,配置完之后就可以和之前一样使用服务名称来调用接口了。
3. 配置负载均衡策略
Ribbon 默认的策略是轮询,从我们前面讲解的例子输出的结果就可以看出来,Ribbon 中提供了很多的策略,这个在后面会进行讲解。我们通过配置可以指定服务使用哪种策略来进行负载操作。
4. 超时时间
Ribbon 中有两种和时间相关的设置,分别是请求连接的超时时间和请求处理的超时时间,设置规则如下:

请求连接的超时时间

ribbon.ConnectTimeout=2000

请求处理的超时时间

ribbon.ReadTimeout=5000

也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
ribbon-config-demo.ribbon.ConnectTimeout=2000
ribbon-config-demo.ribbon.ReadTimeout=5000
5. 并发参数

最大连接数

ribbon.MaxTotalConnections=500

每个host最大连接数

ribbon.MaxConnectionsPerHost=500
配置 Ribbon 的两种方式
1.配置文件方式配置 Ribbon
通过配置文件的方式来为 Ribbon 指定对应的配置:
.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer(负载均衡器操作接口)
.ribbon.NFLoadBalancerRuleClassName: Should implement IRule(负载均衡算法)
.ribbon.NFLoadBalancerPingClassName: Should implement IPing(服务可用性检查)
.ribbon.NIWSServerListClassName: Should implement ServerList(服务列表获取)
.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter(服务列表的过滤)
2.代码配置 Ribbon
通过代码的方式来配置。通过代码方式来配置之前自定义的负载策略,首先需要创建一个配置类,初始化自定义的策略,代码如下所示。
@Configuration
public class BeanConfiguration {
@Bean
public MyRule rule() {
return new MyRule();
}
}
创建一个 Ribbon 客户端的配置类,关联 BeanConfiguration,用 name 来指定调用的服务名称,代码如下所示。
@RibbonClient(name = “ribbon-config-demo”, configuration = BeanConfiguration.class)
public class RibbonClientConfig {

}
可以去掉之前配置文件中的策略配置,然后重启服务,访问接口即可看到和之前一样的效果。
重试机制
在集群环境中,用多个节点来提供服务,难免会有某个节点出现故障。用 Nginx 做负载均衡的时候,如果你的应用是无状态的、可以滚动发布的,也就是需要一台台去重启应用,这样对用户的影响其实是比较小的,因为 Nginx 在转发请求失败后会重新将该请求转发到别的实例上去。
由于 Eureka 是基于 AP 原则构建的,牺牲了数据的一致性,每个 Eureka 服务都会保存注册的服务信息,当注册的客户端与 Eureka 的心跳无法保持时,有可能是网络原因,也有可能是服务挂掉了。
在这种情况下,Eureka 中还会在一段时间内保存注册信息。这个时候客户端就有可能拿到已经挂掉了的服务信息,故 Ribbon 就有可能拿到已经失效了的服务信息,这样就会导致发生失败的请求。
这种问题我们可以利用重试机制来避免。重试机制就是当 Ribbon 发现请求的服务不可到达时,重新请求另外的服务。

  1. RetryRule 重试
    解决上述问题,最简单的方法就是利用 Ribbon 自带的重试策略进行重试,此时只需要指定某个服务的负载策略为重试策略即可:
    ribbon-config-demo.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
  2. Spring Retry 重试
    除了使用 Ribbon 自带的重试策略,我们还可以通过集成 Spring Retry 来进行重试操作。
    在 pom.xml 中添加 Spring Retry 的依赖,代码如下所示。
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

配置重试次数等信息:

对当前实例的重试次数

ribbon.maxAutoRetries=1

切换实例的重试次数

ribbon.maxAutoRetriesNextServer=3

对所有操作请求都进行重试

ribbon.okToRetryOnAllOperations=true

对Http响应码进行重试

ribbon.retryableStatusCodes=500,404,502
JAVA 项目中接口调用怎么做?
1)Httpclient
HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 Http 协议的客户端编程工具包,并且它支持 HTTP 协议最新版本和建议。
HttpClient 相比传统 JDK 自带的 URLConnection,提升了易用性和灵活性,使客户端发送 HTTP 请求变得容易,提高了开发的效率。
2)Okhttp
一个处理网络请求的开源项目,是安卓端最火的轻量级框架,由 Square 公司贡献,用于替代 HttpUrlConnection 和 Apache HttpClient。OkHttp 拥有简洁的 API、高效的性能,并支持多种协议(HTTP/2 和 SPDY)。
3)HttpURLConnection
HttpURLConnection 是 Java 的标准类,它继承自 URLConnection,可用于向指定网站发送 GET 请求、POST 请求。HttpURLConnection 使用比较复杂,不像 HttpClient 那样容易使用。
4)RestTemplate
RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 HTTP 服务的方法,能够大大提高客户端的编写效率。
Feign
上面介绍的是最常见的几种调用接口的方法,我们下面要介绍的方法比上面的更简单、方便,它就是 Feign。
Feign 是一个声明式的 REST 客户端,它能让 REST 调用更加简单。Feign 供了 HTTP 请求的模板,通过编写简单的接口和插入注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。而 Feign 则会完全代理 HTTP 请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Spring Cloud 对 Feign 进行了封装,使其支持 SpringMVC 标准注解和 HttpMessageConverters。Feign 可以与 Eureka 和 Ribbon 组合使用以支持负载均衡。
在Spring Cloud中集成Feign
在 Spring Cloud 中集成 Feign 的步骤相当简单,首先还是加入 Feign 的依赖,代码如下所示。

org.springframework.cloud
spring-cloud-starter-openfeign

在启动类上加 @EnableFeignClients 注解,如果你的 Feign 接口定义跟你的启动类不在同一个包名下,还需要制定扫描的包名 @EnableFeignClients(basePackages=“com.fangjia.api.client”),代码如下所示。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = “com.fangjia.api.client”)
public class FshSubstitutionServiceApplication {
public static void main(String[] args) {
SpringApplication.run(FshSubstitutionServiceApplication.class, args);
}
}
使用Feign调用接口
定义一个 Feign 的客户端,以接口形式存在,代码如下所示。
@FeignClient(value = “eureka-client-user-service”)
public interface UserRemoteClient {
@GetMapping("/user/hello")
String hello();
}
首先我们来看接口上加的 @FeignClient 注解。这个注解标识当前是一个 Feign 的客户端,value 属性是对应的服务名称,也就是你需要调用哪个服务中的接口。
定义方法时直接复制接口的定义即可,当然还有另一种做法,就是将接口单独抽出来定义,然后在 Controller 中实现接口。
在调用的客户端中也实现了接口,从而达到接口共用的目的。我这里的做法是不共用的,即单独创建一个 API Client 的公共项目,基于约定的模式,每写一个接口就要对应写一个调用的 Client,后面打成公共的 jar,这样无论是哪个项目需要调用接口,只要引入公共的接口 SDK jar 即可,不用重新定义一遍了。
定义之后可以直接通过注入 UserRemoteClient 来调用,这对于开发人员来说就像调用本地方法一样。
接下来采用 Feign 来调用 /user/hello 接口,代码如下所示。
@Autowired
private UserRemoteClient userRemoteClient;
@GetMapping("/callHello")
public String callHello() {
//return restTemplate.getForObject(“http://localhost:8083/house/hello”,String.class);
//String result = restTemplate.getForObject(“http://eureka-client-user-service/user/hello”,String.class);
String result = userRemoteClient.hello();
System.out.println(“调用结果:” + result);
return result;
}
通过跟注释掉的代码相比可以发现,我们的调用方式变得越来越简单了,从最开始的指定地址,到后面通过 Eureka 中的服务名称来调用,再到现在直接通过定义接口来调用。
Spring Cloud Feign的自定义配置及使用
Feign 提供了很多的扩展机制,让用户可以更加灵活的使用,这节我们来学习 Feign 的一些自定义配置。
1.日志配置
有时候我们遇到 Bug,比如接口调用失败、参数没收到等问题,或者想看看调用性能,就需要配置 Feign 的日志了,以此让 Feign 把请求信息输出来。
首先定义一个配置类,代码如下所示。
@Configuration
public class FeignConfiguration {
/**
* 日志级别
*
* @return
*/
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
通过源码可以看到日志等级有 4 种,分别是:
NONE:不输出日志。
BASIC:只输出请求方法的 URL 和响应的状态码以及接口执行的时间。
HEADERS:将 BASIC 信息和请求头信息输出。
FULL:输出完整的请求信息。
Feign 日志等级源码如下图所示:
public enum Level {
NONE,
BASIC,
HEADERS,
FULL
}
配置类建好后,我们需要在 Feign Client 中的 @FeignClient 注解中指定使用的配置类,代码如下所示。
@FeignClient(value = “eureka-client-user-service”, configuration = FeignConfiguration. class)
public interface UserRemoteClient {
// …
}
在配置文件中执行 Client 的日志级别才能正常输出日志,格式是“logging.level.client 类地址=级别”。
logging.level.net.biancheng.feign_demo.remote.UserRemoteClient=DEBUG
最后通过 Feign 调用我们的 /user/hello 接口,就可以看到控制台输出的调用信息了,如图 所示。

2.契约配置
Spring Cloud 在 Feign 的基础上做了扩展,可以让 Feign 支持 Spring MVC 的注解来调用。原生的 Feign 是不支持 Spring MVC 注解的,原生的使用方法我们在后面会讲解。
如果你想在 Spring Cloud 中使用原生的注解方式来定义客户端也是可以的,通过配置契约来改变这个配置,Spring Cloud 中默认的是 SpringMvcContract,代码如下所示。
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
}
当你配置使用默认的契约后,之前定义的 Client 就用不了,之前上面的注解是 Spring MVC 的注解。
3.Basic 认证配置
通常我们调用的接口都是有权限控制的,很多时候可能认证的值是通过参数去传递的,还有就是通过请求头去传递认证信息,比如 Basic 认证方式。在 Feign 中我们可以直接配置 Basic 认证,代码如下所示。
@Configuration
public class FeignConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor(“user”, “password”);
}
}
或者你可以自定义属于自己的认证方式,其实就是自定义一个请求拦截器。在请求之前做认证操作,然后往请求头中设置认证之后的信息。通过实现 RequestInterceptor 接口来自定义认证方式,代码如下所示。
public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {
public FeignBasicAuthRequestInterceptor() {
}

@Override
public void apply(RequestTemplate template) {
    // 业务逻辑
}

}
然后将配置改成我们自定义的就可以了,这样当 Feign 去请求接口的时候,每次请求之前都会进入 FeignBasicAuthRequestInterceptor 的 apply 方法中,在里面就可以做属于你的逻辑了,代码如下所示。
@Configuration
public class FeignConfiguration {
@Bean
public FeignBasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new FeignBasicAuthRequestInterceptor();
}
}
4.超时时间配置
通过 Options 可以配置连接超时时间和读取超时时间(代码如下所示),Options 的第一个参数是连接超时时间(ms),默认值是 10×1000;第二个是取超时时间(ms),默认值是 60×1000。
@Configuration
public class FeignConfiguration {
@Bean
public Request.Options options() {
return new Request.Options(5000, 10000);
}
}
5.客户端组件配置
Feign 中默认使用 JDK 原生的 URLConnection 发送 HTTP 请求,我们可以集成别的组件来替换掉 URLConnection,比如 Apache HttpClient,OkHttp。
配置 OkHttp 只需要加入 OkHttp 的依赖,代码如下所示。

io.github.openfeign
feign-okhttp

然后修改配置,将 Feign 的 HttpClient 禁用,启用 OkHttp,配置如下:
#feign 使用 okhttp
feign.httpclient.enabled=false
feign.okhttp.enabled=true
关于配置可参考源码 org.springframework.cloud.openfeign.FeignAutoConfiguration。
HttpClient 自动配置源码如下所示:
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass(“com.netflix.loadbalancer.ILoadBalancer”)
@ConditionalOnProperty(value = “feign.httpclient.enabled”, matchIfMissing = true)
protected static class HttpClientFeignConfiguration {

@Autowired(required = false)
private HttpClient httpClient;

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient() {
    if (this.httpClient != null) {
        return new ApacheHttpClient(this.httpClient);
    }
    return new ApacheHttpClient();
}

}
OkHttp 自动配置源码如下所示:
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass(“com.netflix.loadbalancer.ILoadBalancer”)
@ConditionalOnProperty(value = “feign.okhttp.enabled”, matchIfMissing = true)
protected static class OkHttpFeignConfiguration {

@Autowired(required = false)
private okhttp3.OkHttpClient okHttpClient;

@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient() {
    if (this.okHttpClient != null) {
        return new OkHttpClient(this.okHttpClient);
    }
    return new OkHttpClient();
}

}
上面所示两段代码分别是配置 HttpClient 和 OkHttp 的方法。其通过 @ConditionalOnProperty 中的值来决定启用哪种客户端(HttpClient 和 OkHttp),@ConditionalOnClass 表示对应的类在 classpath 目录下存在时,才会去解析对应的配置文件。
6.GZIP 压缩配置
开启压缩可以有效节约网络资源,提升接口性能,我们可以配置 GZIP 来压缩数据:
feign.compression.request.enabled=true
feign.compression.response.enabled=true
还可以配置压缩的类型、最小压缩值的标准:
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
只有当 Feign 的 Http Client 不是 okhttp3 的时候,压缩才会生效,配置源码在 org.spring-framework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,代码如下所示。
@Configuration
@EnableConfigurationProperties(FeignClientEncodingProperties.class)
@ConditionalOnClass(Feign.class)
@ConditionalOnBean(Client.class)
@ConditionalOnProperty(value = “feign.compression.response.enabled”, matchIfMissing = false)
@ConditionalOnMissingBean(type = “okhttp3.OkHttpClient”)
@AutoConfigureAfter(FeignAutoConfiguration.class)
public class FeignAcceptGzipEncodingAutoConfiguration {
@Bean
public FeignAcceptGzipEncodingInterceptor feignAcceptGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
return new FeignAcceptGzipEncodingInterceptor(properties);
}
}
核心代码就是 @ConditionalOnMissingBean(type=“okhttp3.OkHttpClient”),表示 Spring BeanFactory 中不包含指定的 bean 时条件匹配,也就是没有启用 okhttp3 时才会进行压缩配置。
7.GZIP 压缩配置
开启压缩可以有效节约网络资源,提升接口性能,我们可以配置 GZIP 来压缩数据:
feign.compression.request.enabled=true
feign.compression.response.enabled=true
还可以配置压缩的类型、最小压缩值的标准:
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
只有当 Feign 的 Http Client 不是 okhttp3 的时候,压缩才会生效,配置源码在 org.spring-framework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,代码如下所示。
@Configuration
@EnableConfigurationProperties(FeignClientEncodingProperties.class)
@ConditionalOnClass(Feign.class)
@ConditionalOnBean(Client.class)
@ConditionalOnProperty(value = “feign.compression.response.enabled”, matchIfMissing = false)
@ConditionalOnMissingBean(type = “okhttp3.OkHttpClient”)
@AutoConfigureAfter(FeignAutoConfiguration.class)
public class FeignAcceptGzipEncodingAutoConfiguration {

@Bean
public FeignAcceptGzipEncodingInterceptor feignAcceptGzipEncodingInterceptor(
        FeignClientEncodingProperties properties) {
    return new FeignAcceptGzipEncodingInterceptor(properties);
}

}
核心代码就是 @ConditionalOnMissingBean(type=“okhttp3.OkHttpClient”),表示 Spring BeanFactory 中不包含指定的 bean 时条件匹配,也就是没有启用 okhttp3 时才会进行压缩配置。
8.编码器解码器配置
Feign 中提供了自定义的编码解码器设置,同时也提供了多种编码器的实现,比如 Gson、Jaxb、Jackson。我们可以用不同的编码解码器来处理数据的传输。如果你想传输 XML 格式的数据,可以自定义 XML 编码解码器来实现获取使用官方提供的 Jaxb。
配置编码解码器只需要在 Feign 的配置类中注册 Decoder 和 Encoder 这两个类即可,代码如下所示。
@Bean
public Decoder decoder() {
return new MyDecoder();
}
@Bean
public Encoder encoder() {
return new MyEncoder();
}
使用配置自定义 Feign 的配置
除了使用代码的方式来对 Feign 进行配置,我们还可以通过配置文件的方式来指定 Feign 的配置。

链接超时时间

feign.client.config.feignName.connectTimeout=5000

读取超时时间

feign.client.config.feignName.readTimeout=5000

日志等级

feign.client.config.feignName.loggerLevel=full

重试

feign.client.config.feignName.retryer=com.example.SimpleRetryer

拦截器

feign.client.config.feignName.requestInterceptors[0]=com.example.FooRequestInterceptor
feign.client.config.feignName.requestInterceptors[1]=com.example.BarRequestInterceptor

编码器

feign.client.config.feignName.encoder=com.example.SimpleEncoder

解码器

feign.client.config.feignName.decoder=com.example.SimpleDecoder

契约

feign.client.config.feignName.contract=com.example.SimpleContract
继承特性
Feign 的继承特性可以让服务的接口定义单独抽出来,作为公共的依赖,以方便使用。
创建一个 Maven 项目 feign-inherit-api,用于存放 API 接口的定义,增加 Feign 的依赖,代码如下所示。

org.springframework.cloud
spring-cloud-starter-openfeign

定义接口,指定服务名称,代码如下所示。
@FeignClient(“feign-inherit-provide”)
public interface UserRemoteClient {

@GetMapping("/user/name")
String getName();

}
创建一个服务提供者 feign-inherit-provide,引入 feign-inherit-api,代码如下所示。

net.biancheng
feign-inherit-api
0.0.1-SNAPSHOT

实现 UserRemoteClient 接口,代码如下所示。
@RestController
public class DemoController implements UserRemoteClient {
@Override
public String getName() {
return “zhangsan”;
}
}
创建一个服务消费者 feign-inherit-consume,同样需要引入 feign-inherit-api 用于调用 feign-inherit-provide 提供的 /user/name 接口,代码如下所示。
@RestController
public class DemoController {
@Autowired
private UserRemoteClient userRemoteClient;

@GetMapping("/call")
public String callHello() {
    String result = userRemoteClient.getName();
    System.out.println("getName调用结果:" + result);
}

}
通过将接口的定义单独抽出来,服务提供方去实现接口,服务消费方直接就可以引入定义好的接口进行调用,非常方便。
多参数请求构造
多参数请求构造分为 GET 请求和 POST 请求两种方式,首先来看 GET 请求的多参数请求构造方式,代码如下所示。
@GetMapping("/user/info")
String getUserInfo(@RequestParam(“name”)String name,@RequestParam(“age”)int age);
另一种是通过 Map 来传递多个参数,参数数量可以动态改变,在这里还是推荐大家用固定的参数方式,不要用 Map 来传递参数,Map 传递参数最大的问题是可以随意传参。代码如下所示。
@GetMapping("/user/detail")
String getUserDetail(@RequestParam Map param);
POST 请求多参数就定义一个参数类,通过 @RequestBody 注解的方式来实现,代码如下所示。
@PostMapping("/user/add")
String addUser(@RequestBody User user);
实现类中也需要加上 @RequestBody 注解,代码如下所示。
@RestController
public class DemoController implements UserRemoteClient {
@Override
public String addUser(@RequestBody User user) {
return user.getName();
}
}
注意:使用继承特性的时候实现类也需要加上 @RequestBody 注解。
Spring Cloud Hystrix(熔断器)介绍及使用
Hystrix 是 Netflix 针对微服务分布式系统采用的熔断保护中间件,相当于电路中的保险丝。
在分布式环境中,许多服务依赖项中的一些必然会失败。Hystrix 是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的访问点、停止级联失败和提供回退选项来实现这一点,所有这些都可以提高系统的整体弹性。
在微服务架构下,很多服务都相互依赖,如果不能对依赖的服务进行隔离,那么服务本身也有可能发生故障,Hystrix 通过 HystrixCommand 对调用进行隔离,这样可以阻止故障的连锁效应,能够让接口调用快速失败并迅速恢复正常,或者回退并优雅降级。
Hystrix 的简单使用
创建一个空的 Maven 项目,在项目中增加 Hystrix 的依赖,代码如下所示。

com.netflix.hystrix
hystrix-core
1.5.18

编写第一个 HystrixCommand,代码如下所示。
public class MyHystrixCommand extends HystrixCommand {
private final String name;
public MyHystrixCommand(String name) {
super(HystrixCommandGroupKey.Factory.asKey(“MyGroup”));
this.name = name;
}

@Override
protected String run() {
    return this.name + ":" + Thread.currentThread().getName();
}

}
首先需要继承 HystrixCommand,通过构造函数设置一个 Groupkey。具体的逻辑在 run 方法中,我们返回了一个当前线程名称的值。写一个 main 方法来调用上面编写的 MyHystrixCommand 程序,代码如下所示。
public static void main(String[] args) throws InterruptedException, ExecutionException {
String result = new MyHystrixCommand(“zhangsan”).execute();
System.out.println(result);
}
输出结果如图所示:

从图中可以看到输出结果是“zhangsan:hystrix-MyGroup-1”。由此可以看出,构造函数中设置的组名变成了线程的名字。
上面是同步调用,如果需要异步调用可以使用如下代码所示的方法。
public static void main(String[] args) throws InterruptedException, ExecutionException {
Future future = new MyHystrixCommand(“zhangsan”).queue();
System.out.println(future.get());
}
运行结果和图中结果相同。
回退支持
下面我们通过增加执行时间模拟调用超时失败的情况。首先改造 MyHystrixCommand,增加 getFallback 方法返回回退内容,代码如下所示。
public class MyHystrixCommand extends HystrixCommand {
private final String name;
public MyHystrixCommand(String name) {
super(HystrixCommandGroupKey.Factory.asKey(“MyGroup”));
this.name = name;
}
@Override
protected String run() {
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.name + “:” + Thread.currentThread().getName();
}
@Override
protected String getFallback() {
return "失败了 ";
}
}
重新执行调用代码,运行结果如图所示,可以发现返回的内容是“失败了”,证明已经触发了回退。

Spring Cloud Hystrix资源隔离策略(线程、信号量)
Hystrix 的资源隔离策略有两种,分别为线程池和信号量。那我们为什么需要资源隔离呢?
在一个分布式系统中,服务之间都是相互调用的,例如,我们容器(Tomcat)配置的线程个数为 1000,服务 A-服务 R,其中服务 I 的并发量非常的大,需要 500 个线程来执行,此时,服务 I 又挂了,那么这 500 个线程很可能就夯死了,那么剩下的服务,总共可用的线程为 500 个,随着并发量的增大,剩余服务挂掉的风险就会越来越大,最后导致整个系统的所有服务都不可用,直到系统宕机。
以上就是服务的雪崩效应。Hystrix 就是用来做资源隔离的,比如说,当客户端向服务端发送请求时,给服务 I 分配了 10 个线程,只要超过了这个并发量就走降级服务,就算服务 I 挂了,最多也就导致服务 I 不可用,容器的 10 个线程不可用了,但是不会影响系统中的其他服务。
下面,我们就来具体说下这两种隔离策略。
1.信号量策略配置
用于隔离本地代码或可快速返回的远程调用可以直接使用信号量隔离,降低线程隔离的上下文切换开销。如 memcached,redis。
线程隔离会带来线程开销,有些场景(比如无网络请求场景)可能会因为用开销换隔离得不偿失,为此 hystrix 提供了信号量隔离。
主要适用于并发需求不大的依赖调用,因为如果并发需求较大,相应的信号量的数量就要设置得够大,因为 Tomcat 线程与处理线程为同一个线程,那么这个依赖调用就会占用过多的 Tomcat 线程资源,有可能会影响到其他服务的接收。
信号量策略配置方法代码如下所示。
super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“MyGroup”)) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)));
this.name = name;
}
之前在 run 方法中特意输出了线程名称,通过这个名称就可以确定当前是线程隔离还是信号量隔离。
2.线程隔离策略配置
执行依赖代码的线程与请求线程(比如 Tomcat 线程)分离,请求线程可以自由控制离开的时间,这也是我们通常说的异步编程,Hystrix 是结合 RxJava 来实现的异步编程。
通过为每个包裹了 HystrixCommand 的 API 接口设置独立的、固定大小的线程池(hystrix.threadpool.default.coreSize)来控制并发访问量,当线程饱和的时候可以拒绝服务,防止依赖问题扩散。
系统默认采用线程隔离策略,我们可以通过 andThreadPoolPropertiesDefaults 配置线程池的一些参数,代码如下所示。
public MyHystrixCommand(String name) {
super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(“MyGroup”)) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)).andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(10).withMaxQueueSize(100).withMaximumSize(100)));
this.name = name;
}
线程隔离策略的优点如下:
1.一个依赖调用可以给予一个线程池,这个依赖的异常不会影响其他的依赖。
2.使用线程可以完全隔离业务代码,请求线程可以快速返回。
3.可以完全模拟异步调用,方便异步编程。
线程隔离策略的缺点:使用线程池的缺点主要是增加了计算的开销。每一个依赖调用都会涉及到队列,调度,上下文切换,而这些操作都有可能在不同的线程中执行。

你可能感兴趣的:(框架)