《Spring Cloud微服务架构实战》--Ribbon
4.1 Ribbon 介绍
4.1.1 Ribbon 简介
Ribbon是Netflix下的负载均衡项目,它在集群中为各个客户端的通信提供了支持,它主要实现中间层应用程序的负载均衡。
Ribbon提供以下特性:
- 负载均衡器,可支持插拔式的负载均衡规则。
- 对多种协议提供支持,例如HTTP、TCP、UDP
- 集成了负载均衡功能的客户端。
同为Netflix项目,Ribbon可以与Eureka整合使用,Ribbon同样被集成到Spring Cloud 中,作为spring-cloud-ne由ix项目中的子模块。
Spring Cloud将Ribbon的API进行了封装,
4.1.2 Ribbon 子模块
Ribbon主要有以下三大子模块。
- 客户端
- ribbon-eureka:为Eureka客户端提供的负载均衡实现类。
- 模块提供了含有负载均衡功
4.1.3负载均衡器组件
Ribbon的负载均衡器主要与集群中的各个服务器进行通信,负载均衡器需要提供以下基础功能:
- 维护服务器的IP、DNS名称等信息。
- 根据特定的逻辑在服务器列表中循环。
为了实现负载均衡的基础功能,Ribbon的负载均衡器有以下三大子模块。
- Rule: 一个逻辑组件,这些逻辑将会决定从服务器列表中返回哪个服务器实例。
- Ping:该组件主要使用定时器来确保服务器网络可以连接。
- ServerList:服务器列表,可以通过静态的配置确定负载的服务器,也可以动态指定 服务器列表。如果动态指定服务器列表,则会有后台的线程来刷新该列表。
本章关于Ribbon的知识,主要围绕负载均衡器组件进行
4.2 第一个Ribbon程序
本章的4.2节和4.3节,单独使用Ribbon框架,关于整合Spring Cloud的内容,将在 4.4节讲述。本节将以一个简单的Hello World程序来展示Ribbon API的使用。
本例的程序
本书所使用的Spring Cloud, 默认集成的Ribbon版本为2.2.2, 因此本书也使用该版本的Ribbon
4.2.1编写服务
为了能查看负载均衡效果,先编写一个简单的REST服务,通过指定不同的端口,让服务可以启动多个实例。本例的请求服务器,仅仅是一个基于Spring Boot的Web应用,
与2.3节中的应用类似,如果读者熟悉建立过程,可跳过部分创建过程,本小节最终目的
新建名称为first-ribbon-server的Maven项目,加入以下依赖:
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-web</artifactld>
<version>l.5.4.RELEASE</version>
</dependency>
建立Spring Boot启动类,如代码所示:
@SpringBootApplication
public class FirstServerApplication {
public static void main(String[] args) {
//读取控制台输入作为端口参数
Scanner scan = new Scanner(System.in);
String port = scan.nextLine();
//设置启动的服务器端口
new SpringApplicationBuiIder(FirstServerApplication.class).properties("server.port=" + port).run(args);
}
}
运行main方法,并在控制台输入端口号,即可启动Web服务器
接下来编写控制器:
@RestController
public class MyController {
@RequestMapping(value = "/person/{personld}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Person findPerson(@PathVariable("personld") Integer personld, HttpServletRequest request) {
Person p = new Person ();
p.setld(personld);
p.setName("Crazyit");
p.setAge(30);
p.setMessage(request.getRequestURL().toString());
return p;
}
@RequestMapping(value = ”/”, method = RequestMethod.GET)
public String hello() {
return "hello”;
}
}
的服务后, 会返回一个Person实例的JSON字符串,为了看到请求的URL,为Person的message属性设置了请求的URL
4.2.2 编写请求客户端
新建名称为first-ribbon-client的Maven项目,加入以下依赖:
<dependency>
<groupld>com.netflix.ribbon</groupld>
<artifactld>ribbon</artifactld>
<version>2.2.2</version>
</dependency>
<dependency>
<groupld>com.netflix.ribbon</groupld>
<artifactld>ribbon-httpclient</artifactld>
<version>2.2.2</version>
</dependency>
接下来,使用Ribbon的客户端发送请求, 请见代码:
public class TestRestClient {
public static void main(String[] args) throws Exception {
//设置请求的服务器
ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.listOfServers", "localhost:8080,localhost:8081");
//获取REST请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
//创建请求实例
HttpRequest request = HttpRequest.newBuilder()
.uri("/person/l").build();
//发送6次请求到服务器中
for (int i = 0; i < 6; i++) {
HttpResponse response = client.executeWithLoadBalancer(request);
String result = response.getEntity(String.class);
System.out.printIn(result);
}
}
}
为 localhost:8080 与 localhost:8081, 再使用 RestClient 对象,向/person/1 地址发送
启动两次服务器类FirstServerApplication,并在控制台分别输入8080和8081端口。启动服务器后,运行客户端,输出结果如下:
{ "id":1, "name":"Crazyit","age" : 30, "message":"http://localhost: 8081/person/l"}
{"id":1, "name":"Crazyit", "age":30,"message": "http://localhost:8080/person/l"}
{"id.":1, "name":"Crazyit","age":30,"message": "http://localhost: 8081/person/l"}
("id":1, "name":"Crazyit","age”:30,"message":"http://localhost:8080/person/l"}
{"id":l, "name":"Crazyit","age":30,"message":"http://localhost:8081/person/l"}
("id":l, "name":"Crazyit","age":30,"message":"http://localhost:8080/person/l"}
根据输出结果可知,RestClient轮流向8080与8081端口发送请求,可见在RestClient 中己经帮我们实现了负载均衡的功能
4.2.3 Ribbon 的配置
在编写客户端时,使用了 ConfigurationManager来设置配置项,除了在代码中指定配置项外,还可以将配置放到.properties 文件中。
ConfigurationManager 的
<client><nameSpace>.<property>=<value>
时可 传入客户端的名称,即可返回对应的“请求客户端”实例。
<nameSpace>为该配置的命名空间,默认为ribbon,
<property>为属性名,<value>为属性值。
如果想对全部客户端生效, 可以将客户端名称去掉,直接以<namespace>.<property>的格式进行配置。以下的配置为客
my-client.ribbon.listOfServers=localhost:8080,localhost:8081
的配置文件(即application.yml)中使用。
4.3 Ribbon的负载均衡机制
Ribbon提供了几个负载均衡的组件,其目的就是让请求转给合适的服务器处理。因此, 如何选择合适的服务器便成为负载均衡机制的核心。本节将围绕Ribbon负载均衡器的组件, 向大家展示Ribbon负载均衡的实现机制。
4.3.1负载均衡器
Ribbon的负载均衡器接口定义了服务器的操作,主要是用于进行服务器选择。
在前面 的例子中,客户端使用了 RestClient类,在发送请求时,会使用负载均衡器(ILoadBalancer) 接口,根据特定的逻辑来选择服务器。
服务器列表可使用listOfServers进行配置,也可以
public class ChoseServerLest{
//创建负载均衡器
ILoadBalancer lb = new BaseLoadBalancer();
//添加服务器
List<Server> servers = new ArrayList<Server>();
servers.add(new Server("localhost", 8080));
servers.add(new Server("localhost", 8081));
lb.addServers(servers);
//进行6次服务器选择
for(int i = 0; i < 6; i++) {
Server s = lb.chooseServer(null);
System.out.printIn(s);
}
}
代码中使用了 BaseLoadBalancer这个负载均衡器,将两个服务器对象加入负载均衡器
localhost:8081
localhost:8080
localhost:8081
localhost:8080
localhost:8081
localhost:8080
节的例 子选择服务器的逻辑是一致的,在默认情况下,会使用RoundRobinRule的规则逻辑。
4.3.2自定义负载规则
根据前一小节的介绍可知,选择哪个服务器进行请求处理,由ILoadBalancer接口的
而在BaseLoadBalancer类中,则使用IRule接口的choose方法来 决定选择哪一个服务器对象。
如果想自定义负载均衡规定,可以编写一个IRule接口的实 现类。
如下代码清单实现了自己的负载规定:
public class MyRule implements IRule {
ILoadBalancer lb;
public MyRule() {
}
public MyRule(ILoadBalancer lb) {
this.lb = lb;
}
public Server choose(Object key) {
//获取全部的服务器
List<Server> servers = lb.getAHServers ();
//只返回第一个Server对象
return servers.get(0);
}
public void setLoadBalancer(ILoadBalancer lb) {
this.lb = lb;
}
public ILoadBalancer getLoadBalancer () {
return this.lb;
}
}
在自定义规则类中,实现的choose方法调用了 ILoadBalancer的getAHServers方法, 返回全部服务器,为了简单起见,本例只返回第一个服务器。
为了能在负载均衡器中使用
public class TestMyRule{
//创建负载均衡器
BaseLoadBalancer lb = new BaseLoadBalancer();
//设置自定义的负载规则
lb.setRule(new MyRule(lb));
//添加服务器
List<Servet> servers = new ArrayList<Server>();
servers.add(new Server("localhost", 8080));
servers.add(new Server("localhost", 8081));
lb.addServers(servers);
//进行6次服务器选择
for(int i = 0; i < 6; i++) {
Server s = lb.chooseServer(null);
System.out.printIn(s);
}
}
运行上面代码清单可以看到,请求6次所得到的服务器均为localhost:8080。
以上是直 接使用编码方式来设置负载规则,可以使用配置的方式来完成这些工作。修改Ribbon的配
public class TestMyRuleConfig{
//设置请求的服务器
ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.listOfServers","localhost:8080,localhost:8081");
//配置规则处理类
ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.NFLoadBalancerRuleClassName", MyRule.class.getName());
//获取rest请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
//创建请求实例
HttpRequest request = HttpRequest.newBuilder().uri("/person/1").build();
//发送6次请求到服务器中
for (int i = 0; i < 6; i++) {
HttpResponse response = client.executeWithLoadBalancer(request);
String result = response.getEntity(String.class); System.out.printIn(result);
}
}
节中介绍的客户端基本一致,只是加入了
这个配置项同样可以在配置文件中使用,包括Spring Cloud的配置文件(application.yml等)。
可以看到输出了 6 次 {"id":l,"name":"Crazyit","age":30,"message":"http://localhost:8080/person/l"},
根据结果可知,我们的自定义规则生效了,请求只让8080端口处理。
在实际环境中,如果要实现自定义的负载规则,可能还需要结合各种因素,例如考虑:
实现中可能还涉及使用计算器、数据库等技术,具体情形会更为复杂,本例的负载规则较为简单,目的是让读者了解负载均衡的原理。
4.3.3 Ribbon自带的负载规则
Ribbon提供了若干个内置的负载规则,使用者完全可以直接使用,主要有以下内置的负载规则。
> RoundRobinRule:系统默认的规则,通过简单地轮询服务列表来选择服务器,其他
> AvailabilityFilteringRule:该规则会忽略以下服务器。
- 无法连接的服务器:在默认情况下,如果3次连接失败,该服务器将会被置为“短路“的状态,该状态将持续30秒;如果再次连接失败,“短路”状态的持续时间
可以通过修改niws.loadbalancer.<clientName>.connection- FailureCountThreshold属性,来配置连接失败的次数。
- 并发数过高的服务器:如果连接到该服务器的并发数过高,也会被这个规则 忽略,可以通过修改<clientName>.ribbon.ActiveConnectionsLimit 属性来设定
> WeightedResponseTimeRule:为每个服务器赋予一个权重值,服务器的响应时间越 长,该权重值就越少,这个规则会随机选择服务器,权重值有可能会决定服务器的
> ZoneAvoidanceRule:该规则以区域、可用服务器为基础进行服务器选择。使用Zone 对服务器进行分类,可以理解为机架或者机房。
> BestAvailableRule:忽略“短路”的服务器,并选择并发数较低的服务器。
> RandomRule:顾名思义,随机选择可用的服务器。
> RetryRule:含有重试的选择逻辑,如果使用RoundRobinRule选择的服务器无法连接,那么将会重新选择服务器。
以上提供的负载规则基本可以满足大部分的需求,如果有更为复杂的要求,建议实现
4.3.4 Ping 机制
在负载均衡器中,提供了 Ping机制,每隔一段时间,会去Ping服务器,判断服务器
该工作由IPing接口的实现类负责,如果单独使用Ribbon,在默认情况下,不
如下代码清单使用了另外一个IPing实现类
public class TestPingUrl{
//创建负载均衡器
BaseLoadBalancer lb = new BaseLoadBalancer();
//添加服务器
List<Server> servers = new ArrayList<Server>();
// 8080端口连接正常
servers.add(new Server("localhost", 8080));
// 一个不存在的端口
servers.add(new Server("localhost", 8888));
lb.addServers(servers);
//设置IPing实现类
lb.setPing(new PingUrl());
//设置Ping时间间隔为2秒
lb.setPinglnterval(2);
Thread.sleep(6000);
for (Server s : lb.getAHServers()) {
System.out.printin(s.getHostPort() + "状态:"+s.isAlive());
}
}
秒就向 两个服务器发起请求,PingUrl实际使用的是HttpCliento
在以上例子中,实际上会请求 http://localhost:8080 与 http://localhost:8888 这两个地址厂在运行前先以 8080 端口启动 4.2 节中介绍的服务器,
最终效果为8080的服务器状态正常,而8888的服务器则无法连接,
localhost: 8080 状态:true
localhost: 8888 状态:false
除了在代码中配置使用IPing类外,还可以在配置中设置IPing实现类,请见代码:
public class IestPingUrlConfig{
//设置请求的服务器
ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.1istOfServers",
"localhost:8080,localhost:8 8 8 8");
//配置Ping处理类
ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.NFLoadBalancerPingClassName",
PingUrl.class.getName());
//配置Ping时间间隔
ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.NFLoadBalancerPinglnterval",
2);
//获取rest请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
Thread.sleep (6000);
//获取全部服务器
List<Server> servers = client.getLoadBalancer().getAHServers();
System.out.printIn(servers.size());
//输出状态
for(Server s : servers) (
System.out.printin(s.getHostPort() + ”状态:" + s.isAlive());
}
}
注意代码中的以下两个配置。
> my-client.ribbon.NFLoadBalancerPingClassName:配置 IPing 的实现类。
> my-client.ribbon.NFLoadBalancerPinglnterval:配置 Ping 操作的时间间隔。
以上两个配置同样可以使用在配置文件中。
4.3.5 自定义
通过前面章节的案例可知,实现自定义Ping较为简单,先实现IPing接口,然后再通过配置来设定具体的Ping实现类,下面代码清单为自定义的Ping类
public class MyPing implements IPing {
public boolean isAlive(Server server) {
System.out.println("这是自定义Ping实现类:" + server.getHostPort());
return true;
}
}
要使用自定义的
4.3.6其他配置
本节主要介绍了
除了这两部分外,还可以使用以下配置来改变负载均衡器的其他行为。
- NFLoadBalancerClassName:指定负载均衡器的实现类,可利用该配置实现自己的
- NIWSServerListClassName:服务器列表处理类,用来维护服务器列表,Ribbon已
- NIWSServerListFilterClassName:用于处理服务器列表拦截。
4.4 在 Spring Cloud 中使用
Spring Cloud集成了 Ribbon,结合Eureka,可实现客户端的负载均衡。
前面章节中所使 用的RestTemplate (被@LoadBalanced修饰)、还有后面章节中将介绍的Feign,都己经拥有负载均衡功能。
本节将以RestTemplate为基础,讲述及测试Eureka中的Ribbon配置
4.4.1 准备工作
为了本节的测试做准备,按顺序进行以下工作:
>新建Eureka服务器端项目,命名为cloud-server,端口为8761,代码目录为 codes\04\4.4\cloud-server
>新建Eureka服务提供者项目,命名为cloud-provider,代码目录为codes\04\4.4\cloud- provider,该项目主要进行以下工作。
- 在控制器里面发布一个REST服务,地址为/person/{personld},请求后返回 Person 实例,其中 Person 的 message 为 HTTP 请求的
- 服务提供者需要启动两次,因此在控制台中需要输入启动端口
>新建Eureka服务调用者项目,命名为cloud.invoker,对外端口为9000,代码目录
以上项目准备完成并启动后,结构如图
4.4.2 使用代码配置Ribbon
在4.3节中讲述了负载规则以及Ping机制,在Spring Cloud中,可将自定义的负载规 则以及Ping类放到服务调用者中查看效果。新建自定义的IRule与IPing,两个实现类请见如下代码清单:
public class MyRule implements IRule {
private ILoadBalancer lb;
public Server choose(Object key) {
List<Server> servers = lb.getAHServers ();
System.out.printin ("这是自定义服务器规则类,输出服务器信息:");
for(Server s : servers) {
System.out.printin(n ” + s.getHostPort());
}
return servers.get(0);
}
//省略setter和getter方法
}
public class MyPing implements IPing {
public boolean isAlive(Server server) {
System.out.printin("自定义Ping类,服务器信息:"+ server.getHostPort());
return true;
}
}
根据两个自定义的IRule和IPing类可知,实际上跟4.3节中介绍的自定义实现类似, 服务器选择规则只返回集合中的第一个实例,IPing实现仅仅是控制输入服务器信息。
接下来,新建配置类、返回规则与Ping的Bean,请见如下代码清单:
public class MyConfig {
@Bean
public IRule getRule() (
return new MyRule();
}
@Bean
public IPing getPing() (
return new MyPing();
}
}
@RibbonClient(name="cloud-provider", configuration=MyConfig.class)
public class CloudProviderConfig {
}
在代码清单4-12中,CloudProviderConfig配置类使用了 @RibbonClient注解,配置了 RibbonClient的名称为cloud-provider,对应的配置类为MyConfig ,也就是名称为
在服务调用者的控制器中,加入对外服务,服务中调用RestTemplate,如代码清单:
@RestController
@Configuration
public class InvokerController {
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
@RequestMapping(value = "/router", method = RequestMethod.GET,
produces = MediaType.APPLICAT10N_JSON_VALUE)
public String router() {
RestTemplate restTpl = getRestTemplate();
//根据名称调用服务
String json = restTpl.getForObject("http://cloud-provider/person/1", String.class);
return json;
}
}
章类似,在此不再赘述。
关于RestTemplate的原理,将在本章后面的章节中讲述。进行以下操作,查看本例效果:
- 启动一个 Eureka 服务器(cloud-server)。
- 启动两次Eureka服务提供者(cloud-provider),分别输入8080与8081端口。
- 启动一个Eureka服务调用者(cloud-invoker)。
- 打开浏览器访问http://Iocalhost:9000/router,可以看到调用服务后返回的JSON字符 串,不管刷新多少次,最终都只会访问其中一个端口。
4.4.3使用配置文件设置Ribbon
时,这些
cloud-provider:
ribbon:
NFLoadBalancerRuleClassName: org.crazyit.cloud.MyRule
NFLoadBalancerPingClassName: org.crazyit,cloud.MyPing
listOfServers: http://localhost:8080/,http://localhost:8081/
类以及服务器列表,以同样的方
4.4.2节使用了代码的方式来设置Ribbon,而4.4.3节则使用配置文件的方式,两种方式的效果一样,但比较起来,明显是配置文件的方式更加简便。
注: 在本案例的cloud-invoker模块中,默认使用了代码的方式来配置Ribbon, 配置文件中的配置已被注释。
4.4.4 Spring 使用 Ribbon 的
Spring Cloud对Ribbon进行封装,例如像负载客户端、负载均衡器等,我们可以直接使用Spring的LoadBalancerClient来处理请求以及服务选择。
下面代码清单在服务器调用 者的控制器中使用了 LoadBalancerClient
public class lnvokerController{
@Autowired
private LoadBalancerClient loadBalancer;
@RequestMapping(value = "/uselb", method = RequestMethod.GET,
produces = MediaType.APPLICATI0N_JSON_VALUE)
public Serviceinstance uselb() {
//査找服务器实例
ServiceInstance si = loadBalancer.choose("cloud-provider");
return si;
}
}
如代码清单
public class lnvokerController{
@Autowired
private SpringClientFactory factory;
@RequestMapping(value = "/defaultValue", method = RequestMethod.GET,
produces = MediaType.APPLICATI0N_JSON_VALUE)
public String defaultvalue() (
System.out.printin ("====输出默认配置:");
//获取默认的配置
ZoneAwareLoadBalancer alb = (ZoneAwareLoadBalancer) factory.getLoadBalancer("default");
System, out .printIn (*' IClientConf ig:"+ factory.getLoadBalancer("default") .getClass ().getName());
System.out.printIn(" IRule: "+alb.getRule() .getClass().getName());
System.out.printin ("IPing: "+alb.getPing().getClass().getName());
System.out.printin ("ServerList:"+alb.getServerListlmpl().getClass().getName());
System, out .printin ("ServerListFilter:"+alb.getFilter().getClass().getName());
System, out .printIn ("ILoadBalancer: "+alb.getClass().getName());
System.out .printIn ("Pinginterval: "+alb.getPinglnterval());
System, out .printin ("====输出 cloud-provider 配置:");
// 获取 cloud-provider 的配置
ZoneAwareLoadBalancer alb2 = (ZoneAwareLoadBalancer) factory.getLoadBalancer("cloud-provider");
System.out.printIn ("IClientConfig:"+ factory.getLoadBalancer("cloud-provider").getClass().getName());
System.out.printIn ("IRule:" + alb2.getRule().getClass().getName());
System.out.printIn ("IPing:" + alb2.getPing().getClass().getName());
System.out.printIn ("ServerList:"+ alb2.getServerListlmpl().getClass().getName());
System.out.printIn("ServerListFilter :"+ alb2.getFilter().getClass().getName());
System.out.printIn ("ILoadBalancer: " + alb2.getClass().getName());
System.out.printIn ("Pinginterval: "+ alb2.getPinglnterval());
return "";
}
}
代码中使用了 SpringClientFactory,通过该实例可获取各个默认的实现类以及配置,分别输出了默认配置以及cloud-provider配置。
运行上面的代码清单,在浏览器中访问地址 http://localhost:8080/defaultValue,可看到控制台中的输出如下:
====输出默认配置:
IClientConfig: com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: com.netflix.loadbalancer.ZoneAvoidanceRule
IPing: com.netflix.niws.loadbalancer.NIWSDiscoveryPing
ServerList: org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingserverList
ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter
ILoadBalancer: com.netflix.loadbalancer.ZoneAwareLoadBalancer
Pinginterval: 30
====输出 cloud-provider 配置:
IClientConfig: com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: org.crazyit.cloud.MyRule
IPing: org.crazyit.cloud.MyPing
ServerList: org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList
ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter
ILoadBalancer: com.netflix.loadbalancer.ZoneAwareLoadBalancer Pinginterval: 30
定义的实现类。
一般情况下,Spring已经帮我们封装好了 Ribbon,我们只需直接调用RestTemplate等
在接下来的章节中,我们将讲述RestTemplate进行负载均衡的原理。
4.5 RestTemplate 负载均衡
4.5.1 @LoadBalanced 注解概述
项目中的—个REST客户端,它遵循REST的设计原则,
RestTemplate本身不具有负载均衡的功能,该类也与Spring Cloud没有关系,但为何加入@LoadBalanced注解后,一个RestTemplate实例就具有负载均衡的功能了呢?实际上这要得益于RestTemplate的拦截器功能。
在 Spring Cloud 中,使用@LoadBalanced修饰的RestTemplate, 在 Spring 容器启动时, 会为这些被修饰过的RestTemplate添加拦截器,拦截器中使用了 LoadBalancerClient来处理请求,
LoadBalancerClient本来就是Spring封装的负载均衡客户端,通过这样间接处理,使得RestTemplate拥有了负载均衡的功能。
本节将模仿拦截器机制,带领大家实现一个简单的RestTemplate,以便让大家更了解 @LoadBalanced 以及 RestTemplate 的原理。
本节的案例只依赖了 spring-boot-starter-web 模块:
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-web</artifactld>
<version>l.5.4.RELEASE</version>
</dependency>
4.5.2编写自定义注解以及拦截器
先模仿@LoadBalanced注解,编写一个自定义注解,请见代码清单:
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyLoadBalanced {
}
限定注解,接下来编写自定义的拦截
public class Mylnterceptor implements ClientHttpRequestInterceptor {
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws lOException {
System.out.printin("这是自定义拦截器实现");
System.out.printin("原来的URI:" + request.getURI());
//换成新的请求对象(更换URI)
MyHttpRequest newRequest = new MyHttpRequest(request);
System.out.printin("拦截后新的URL:"+ request.getURI());
return execution.execute(newRequest, body);
}
}
在自定义拦截器Mylnterceptor中,实现了 intercept方法,该方法会将原来的HttpRequest 对象转换为我们自定义的MyHttpRequest,
MyHttpRequest是一个自定义的请求类,实现请见代码清单:
public class MyHttpRequest implements HttpRequest {
private HttpRequest sourceRequest;
public MyHttpRequest(HttpRequest sourceRequest) {
this.sourceRequest = sourceRequest;
}
public HttpHeaders getHeaders() {
return sourceRequest.getHeaders ();
}
public HttpMethod getMethod() {
return sourceRequest.getMethod();
}
/**
* 将URI转换
*/
public URI getURI() {
try{
String oldUri = sourceRequest.getURI().toString();
URI newUri = new URI(nhttp://localhost:8080/hello");
return newUri;
} catch (Exception e) {
e.printStackTrace();
}
return sourceRequest.getURI();
}
}
在MyHttpRequest类中,会将原来请求的URI进行改写,只要使用了这个对象,所有 的请求都会被转发到
Spring Cloud 在对 RestTemplate 进行拦截的时候也做了同样的事情,只不过并没有像我们这样固定了 URI,而是对“源请 求”进行了更加灵活的处理。
接下来使用自定义注解以及拦截器。
4.5.3使用自定义拦截器以及注解
编写一个Spring的配置类,在初始化的Bean中为容器中的RestTemplate实例设置自 定义拦截器,本例的Spring自动配置类请见代码清单:
@Configuration
public class MyAutoConfiguration {
@Autowired(required=false)
@MyLoadBalanced
private List<RestTemplate> myTemplates = Collections.emptyList();
@Bean
public SmartInitializingsingleton myLoadBalancedRestTemplatelnitializer() {
System.out.printin("这个Bean将在容器初始化时创建");
return new SmartlnitializingSingleton() {
public void afterSingletonsInstantiated() {
for(RestTemplate tpl : myTemplates) (
//创建一个自定义的拦截器实例
My Interceptor mi = new Mylnterceptor ();
//获取RestTemplate原来的拦截器
List list = new ArrayList(tpl.getlnterceptors());
//添加到拦截器集合
list.add(mi);
//将新的拦截器集合设置到RestTemplate实例
tpl.setinterceptors(list);
}
)};
}
}
在配置类中定义了 RestTemplate实例的集合,并且使用了@MyLoadBalanced以及 @Autowired注解进行修饰,@MyLoadBalanced中含有@Qualifier注解。
简单来说,就是在 Spring容器中使用了@MyLoadBalanced修饰的RestTemplate实例,该实例将会被加入配置类的RestTemplate集合中。
在容器初始化时,Spring 会调用 myLoadBalancedRestTemplatelnitializer 方法来创建 Bean, 该Bean在初始化完成后,会遍历RestTemplate集合并为它们设置“自定义拦截器”,
请见 代码清单4-19中的粗体代码。
下面在控制器中使用@MyLoadBalanced来修饰调用者的
4.5.4在控制器中使用RestTemplate
控制器代码请见代码清单
@RestController
@Configuration
public class InvokerController {
@Bean
@MyLoadBalanced
public RestTemplate getMyRestTemplate() {
return new RestTemplate();
}
/**
* 浏览器访问的请求
*/
@RequestMapping(value = "/router", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public String router() {
RestTemplate restTpl = getMyRestTemplate();
//根据名称来调用服务,这个URI会被拦截器所置换
String json = restTpl.getForObject("http://my-server/hello", String.class);
return json;
}
/**
* 最终的请求都会转到这个服务
*/
@RequestMapping(value = "/hello", method = RequestMethod.GET)
@ResponseBody
public String hello () {
return "Hello World";
}
}
注意控制器的hello方法,前面实现的拦截器会将全部请求都转到这个服务中。控制器
熟悉前面使用RestTemplate的读 者可发现,我们实现的注解与Spring提供的@LoadBalanced注解使用方法一致。
在控制器
打开浏览器,访问http://localhost:8080/router,可以看到实际上调用了 hello服务。
在访问该地址时,控制台输出如下:
=============这是自定义拦截器实现
原来的 URI:http://my-server/hello
拦截后新的URI: http://localhost: 8080/hello
Spring Cloud对RestTemplate的拦截实现更加复杂,并且在拦截器中使用 LoadBalancerClient来实现请求的负载均衡功能。
我们在实际环境中,并不需要实现自定义
本节的目的是展示RestTemplate的原理。
4.6本章小结
讲解了 Spring Cloud中的负载均衡组件Ribbon,对于Ribbon如何进行负载均 衡进行了详细讲解。读者学习完本章后,可以了解Ribbon是如何进行负载均衡的,
如果有
作为Spring Cloud中的重要组件,Spring Cloud对Ribbon进行了封装,在使用Spring 提供的API时,我们甚至感觉不到Ribbon的存在。
本章的4.4节,重点讲述了在Spring Cloud 中如何配置和使用Ribbon;
节中,我们都使用了 RestTemplate发送请求,在4.5节,我们讲解了
读者学习完4.5节,可以更加清楚RestTemplate的工作机制,以