一、RestTemplate
1.1簡介
spring框架提供的RestTemplate類可用于在應用中呼叫rest服務,它簡化了與http服務的通信方式,統一了RESTful的標準,封裝了http鏈接, 我們只需要傳入url及回傳值型別即可,相較于之前常用的HttpClient,RestTemplate是一種更優雅的呼叫RESTful服務的方式,
在Spring應用程式中訪問第三方REST服務與使用Spring RestTemplate類有關,RestTemplate類的設計原則與許多其他Spring *模板類(例如JdbcTemplate、JmsTemplate)相同,為執行復雜任務提供了一種具有默認行為的簡化方法,
RestTemplate默認依賴JDK提供http連接的能力(HttpURLConnection),如果有需要的話也可以通過setRequestFactory方法替換為例如 Apache HttpComponents、Netty或OkHttp等其它HTTP library,
考慮到RestTemplate類是為呼叫REST服務而設計的,因此它的主要方法與REST的基礎緊密相連就不足為奇了,后者是HTTP協議的方法:HEAD、GET、POST、PUT、DELETE和OPTIONS,例如,RestTemplate類具有headForHeaders()、getForObject()、postForObject()、put()和delete()等方法,
1.2、實作
最新api地址:https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html
首先建兩個專案

RestTemplate包含以下幾個部分:
-
- HttpMessageConverter 物件轉換器
- ClientHttpRequestFactory 默認是JDK的HttpURLConnection
- ResponseErrorHandler 例外處理
- ClientHttpRequestInterceptor 請求攔截器
spring-cloud-server的配置
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
application.properties
spring.application.name=spring-cloud-server
server.port=8080
RestTemplateServer.class
@RestController public class RestTemplateServer { @Value("${server.port}") private int port; @GetMapping("/orders") public String getAllOrder(){ System.out.println("port:"+port); return "測驗成功"; } }
啟動專案訪問結果如下

spring-cloud-user的組態檔
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
server.port=8088
業務代碼RestTemplateUser.class
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因為RestTemplate不存在所以要注入 @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/user") public String findById(){ return restTemplate.getForObject("http://localhost:8080/orders",String.class); } }
啟動專案訪問可得到8080服務的結果

這樣我們初步完成了兩個獨立專案的通信,如果不想在通過new的方式創建RestTemplate那也可以通過build()方法創建,修改后如下
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因為RestTemplate不存在所以要注入 // @Bean // public RestTemplate restTemplate(){ // return new RestTemplate(); // } @Bean public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder.build(); } @GetMapping("/user") public String findById(){ return restTemplate.getForObject("http://localhost:8080/orders",String.class); } }
但是現在很多服務架構都是多節點的,那么我們就要考慮多節點負載均衡的問題,這時最先想到的是Ribbon,修改代碼
修改cloud-cloud-user的pom.xml檔案,增加
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
為演示負載均衡,啟動兩個spring-cloud-server節點,再配置一個節點并啟動

修改完后,再修改spring-cloud-user組態檔
server.port=8088 spring-cloud-server.ribbon.listOfServers=\ localhost:8080,localhost:8081
這樣玩后有心的人就發現了,業務再用return restTemplate.getForObject("http://localhost:8080/orders",String.class);訪問另一個專案就不合適了,更改RestTemplateUser.class類
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因為RestTemplate不存在所以要注入 // @Bean // public RestTemplate restTemplate(){ // return new RestTemplate(); // } @Bean public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder.build(); } @Autowired LoadBalancerClient loadBalancerClient; @GetMapping("/user") public String findById(){ ServiceInstance serviceInstance=loadBalancerClient.choose("spring-cloud-server"); String url=String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()+"/orders"); return restTemplate.getForObject(url,String.class); //通過服務名稱在組態檔中選擇埠呼叫 // return restTemplate.getForObject("http://localhost:8080/orders",String.class); } }
訪問下面地址,多點幾次



說到 了這里那我們現在就要來看下Ribbon了
二、Ribbon簡介
需要解決的問題: ① 如何在配置Eureka Client注冊中心時不去硬編碼Eureka Server的地址? ② 在微服務不同模塊間進行通信時,如何不去硬編碼服務提供者的地址?③ 當部署多個相同微服務時,如何實作請求時的負載均衡?
實作負載均衡方式1:通過服務器端實作負載均衡(nginx)
實作負載均衡方式2:通過客戶端實作負載均衡

Ribbon是什么? Ribbon是Netflix發布的云中間層服務開源專案,其主要功能是提供客戶端實作負載均衡演算法,Ribbon客戶端組件提供一系列完善的配置項如連接超時,重試等,簡單的說,Ribbon是一個客戶端負載均衡器,我們可以在組態檔中Load Balancer后面的所有機器,Ribbon會自動的幫助你基于某種規則(如簡單輪詢,隨機連接等)去連接這些機器,我們也很容易使用Ribbon實作自定義的負載均衡演算法, 下圖展示了Eureka使用Ribbon時的大致架構:
Ribbon作業時分為兩步:第一步選擇Eureka Server,它優先選擇在同一個Zone且負載較少的Server;第二步再根據用戶指定的策略,再從Server取到的服務注冊串列中選擇一個地址,其中Ribbon提供了很多策略,例如輪詢round robin、隨機Random、根據回應時間加權等,
為了更好的了解Ribbon后面肯定是要進入原始碼,在進入原始碼之前做個鋪墊,我再來改造上面的代碼,引入@LoadBalanced注解,修改下
@RestController public class RestTemplateUser { @Autowired RestTemplate restTemplate; //因為RestTemplate不存在所以要注入 // @Bean // public RestTemplate restTemplate(){ // return new RestTemplate(); // } // @Bean // public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ // return restTemplateBuilder.build(); // } @Bean @LoadBalanced public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder){ return restTemplateBuilder.build(); } // @Autowired // LoadBalancerClient loadBalancerClient; @GetMapping("/user") public String findById(){ // ServiceInstance serviceInstance=loadBalancerClient.choose("spring-cloud-server"); // String url=String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()+"/orders"); // return restTemplate.getForObject(url,String.class); //通過服務名稱在組態檔中選擇埠呼叫 return restTemplate.getForObject("http://spring-cloud-server/orders",String.class); } }
啟動專案后會發現@LoadBalanced也能實作負載均衡,這里面我們就應該進入看下@LoadBalanced到底做了啥,在沒用@LoadBalanced之前getForObject只能識別ip的路徑,并不能識別服務名進行負載均衡,所以我們要看下@LoadBalanced是怎么實作的負載均衡

在看碼源前先劇透下,之前某人說我寫的東西不好看懂,那我這次多花點時間畫圖,restTemplate.getForObject("http://spring-cloud-server/orders",String.class);這個方法他呼叫的是一個服務器名稱,我們知道,如果要訪問一個服務器我們一個具體的路徑才能訪問,那么@LoadBalanced是怎么做到的由一個服務名得到一個具體的路徑呢,這就要說到攔截器,他在呼叫真實路徑前會有攔截器攔截服務器名,然后拿到服務器去決議然后拼接得到一個真實的路徑名稱,然后拿真實路徑去訪問服務,詳細的步驟在原始碼講解中具體分析,
我們點擊@LoadBalanced進入如下圖
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
我們會發現有一個叫@Qualifier的東西,其實這玩意就是一個標記的作用,但為了后面的原始碼分析,這里還是說明下@Qualifiler的用法
我們在spring-cloud-user專案中新建一個Qualifier包,在包中建三個類
public class QualifierTest { private String name; public QualifierTest(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
//@Configuration用于定義配置類,可替換xml組態檔, // 被注解的類內部包含有一個或多個被@Bean注解的方法, // 這些方法將會被AnnotationConfigApplicationContext或 // AnnotationConfigWebApplicationContext類進行掃描, // 并用于構建bean定義,初始化Spring容器, @Configuration public class QualifierConfiguration { @Qualifier @Bean("QualifierTest1") QualifierTest QualifierTest1(){ return new QualifierTest("QualifierTest1"); } @Qualifier @Bean("QualifierTest2") QualifierTest QualifierTest2(){ return new QualifierTest("QualifierTest2"); } }
@RestController public class QualifierController { //@Qualifier作用是找到所有申明@Qualifier標記的實體 @Qualifier @Autowired List<QualifierTest> testClassList= Collections.emptyList(); @GetMapping("/qualifier") public Object test(){ return testClassList; } }
啟動專案訪問介面結果如下

除掉QualifierConfiguration.class中其中一個@Qualifier后重繪介面,會發現結果如下,這兩個結果對比可以證明@Qualifier其實就是一個標記的作用

有了這個概念后我們進入LoadBalancerAutoConfiguration.class這個自動裝配類中會發現有和我剛剛演示一樣的代碼,其實我就是從這個裝配類中抄的,哈哈;

看到這里相信大家就明白了,因為紅框的內容加了@LoadBalanced注解就能使RestTemplate生效是因為@Qualifier注解,有了這個概念接著往下走,在上圖這個自動裝配類中會加載注入所有加了@LoadBalanced注解的RestTemplate,這一步很關鍵,因為后面的攔截器加載跟這一步有關聯;竟然我們來到了LoadBalancerAutoConfiguration,這個自動裝配類來了,那就聊聊這里面的Bean裝配,下面這個圖是Bean的自動裝配程序

首先看自動裝配類攔截器LoadBalancerInterceptor
@Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { //定義一個Bean @Bean public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } //將定義的Bean作為引數傳入 @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors());
//設定攔截器 list.add(loadBalancerInterceptor);
//設定到restTemplate中去 restTemplate.setInterceptors(list); }; } }
@Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated( final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) { return () -> restTemplateCustomizers.ifAvailable(customizers -> {
//對restTemplates進行for回圈,對每一個restTemplate加一個包裝叫RestTemplateCustomizer
//這個包裝的意義是可以對restTemplate再加一個自定義的攔截 for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } }); }
有了上面的包裝,才有下面的攔截的加強
@Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; }
說到這里再將時序圖畫一下,我最初是通過@LoadBalanced注解進入到他的裝配類LoadBalancerAutoConfiguration,然后在LoadBalancerAutoConfiguration裝配類中找到攔截器的加載和增強的,根據這個邏輯畫出的時序圖如下

之前在開篇中還講到過用下面這種方式進行負載均衡訪問,其實針對LoadBalancerClient是一樣的,他里面有一個RibbonAutoConfiguration
@Autowired
LoadBalancerClient loadBalancerClient;
在RibbonAutoConfiguration裝配類中會找到一個代碼如果下,他在裝配類中對LoadBalancerClient進行初始化
@Bean @ConditionalOnMissingBean(LoadBalancerClient.class) public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory()); }
我們看頭檔案,會發現加載了LoadBalancerAutoConfiguration

這時補充下時序圖如下,這就是Bean的加載程序,經過這一程序攔截器就算是加載進去了

有了攔截器后,下一步要看的話肯定就是來看下攔截器到底做了啥,進入LoadBalancerInterceptor攔截器,會發現他會最終進入如下方法
@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);
//將攔截委托給loadBalancer進行實作 return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); }
跟進loadBalancer看下做了啥(LoadBalancerClient注入是在RibbonAutoConfiguration配置類中完成的),跟蹤進去發現最侄訓是呼叫了RibbonLoadBalancerClient

進入execute方法,會發現里面只做了兩件事
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
//獲得負載均衡器 ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
//根據負載均衡器回傳Server,這個Server回傳是指定的某一個地址,其實負載的決議在這里就完成了 Server server = getServer(loadBalancer, hint); if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); return execute(serviceId, ribbonServer, request); }
進入getLoadBalancer看看他做了啥,在看之前先看下他的類關系圖

ILoadBalancer介面:定義添加服務,選擇服務,獲取可用服務,獲取所有服務方法
AbstractLoadBalancer抽像類:定義了一個關于服務實體的分組列舉,包含了三種型別的服務:ALL表示所有服務,STATUS_UP表示正常運行的服務,STATUS_NOT_UP表示下線的服務,
BaseLoadBalancer:
1):類中有兩個List集合,一個List集合用來保存所有的服務實體,還有一個List集合用來保存當前有效的服務實體
2):定義了一個IPingStrategy,用來描述服務檢查策略,IPingStrategy默認實作采用了SerialPingStrategy實作
3):chooseServer方法中(負載均衡的核心方法),呼叫IRule中的choose方法來找到一個具體的服務實體,默認實作是RoundRobinRule
4):PingTask用來檢查Server是否有效,默認執行時間間隔為10秒
5):markServerDown方法用來標記一個服務是否有效,標記方式為呼叫Server物件的setAlive方法設定isAliveFlag屬性為false
6):getReachableServers方法用來獲取所有有效的服務實體串列
7):getAllServers方法用來獲取所有服務的實體串列
8):addServers方法表示向負載均衡器中添加一個新的服務實體串列
DynamicServerListLoadBalancer:主要是實作了服務實體清單在運行期間的動態更新能力,同時提供了對服務實體清單的過濾功能,
ZoneAwareLoadBalancer:主要是重寫DynamicServerListLoadBalancer中的chooseServer方法,由于DynamicServerListLoadBalancer中負責均衡的策略依然是BaseLoadBalancer中的線性輪詢策略,這種策略不具備區域感知功能
NoOpLoadBalancer:不做任何事的負載均衡實作,一般用于占位(然而貌似從沒被用到過),
有了這個概念后我們下面就來重點看BaseLoadBalancer,在嘮嘮之前先補充下時序圖

點擊getLoadBalancer進入如下代碼

在向下寫前,先提前說下ILoadBalancer這個類里面會幫我們做一件事,他會根據負載均衡的一個演算法進行一個負載的選擇,但是在負載之前他會有一個類的初始化程序,在選擇完成后ILoadBalancer實作回傳,然后將ILoadBalancer做為引數傳給Server server = getServer(loadBalancer, hint);在ILoadBalancer中他有一個實作會去呼叫BaseLoadBalancer.chooseServer,它會呼叫rule.choose(),rule的初始化是在ZoneAvoidanceRule中完成的,所以接下來看要分兩部分,ILoadBalancer做為一個負載均衡器,然后getServer會把這個負載均衡器會傳過去后進行一個負載的計算,這個流程說完后可能很多人還在懵逼狀態,那接下來我們就通過代碼來看他的實作,首先看ILoadBalancer的實作是誰
接著上圖來,點擊getLoadBalancer

然后點擊getInstance

@Override public <C> C getInstance(String name, Class<C> type) {
//這里面通過傳送一個name和一個type得到一個實體,這里面是一個工廠模式,我們點擊getInstance選擇它的NamedContextFactory實作進去 C instance = super.getInstance(name, type); if (instance != null) { return instance; } IClientConfig config = getInstance(name, IClientConfig.class); return instantiateWithConfig(getContext(name), type, config); }

public <T> T getInstance(String name, Class<T> type) {
//工廠模式會加載一個context AnnotationConfigApplicationContext context = getContext(name); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) { return context.getBean(type); } return null; }

getContext方法里面是用spring寫的,比較復雜,點擊getContext后如下圖,這里面是有個默認快取的,如果沒有會用createContext(name)根據名稱創建一個快取

回退到AnnotationConfigApplicationContext context = getContext(name);
public <T> T getInstance(String name, Class<T> type) { AnnotationConfigApplicationContext context = getContext(name); if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) {
通過type得到一個Bean return context.getBean(type); } return null; }
再回退到C instance = super.getInstance(name, type);進行打debug看下他回傳的是什么型別的ILoadBalancer

從上圖可以看到回傳的是一個ZoneAwareLoadBalancer的ILoadBalancer,然后就拿著ILoadBalancer傳入getServer(loadBalancer, hint);中,這時的時序圖就如下了

到了這一步獲取負載均衡器這一程序就完成了,下面就是來完成程序2.通過負載均衡器中配置的默認負載均衡演算法選一個合適的Server,我們進入
Server server = getServer(loadBalancer, hint);的getServer方法,點擊進去如下,這里面其實進行的就是針對一個服務節點的選擇,其中loadBalancer.chooseServer(hint != null ? hint : "default");就是一種演算法的選擇,我們這里面沒有選擇演算法,所以采用默認演算法BaseLoadBalancer
進入默認演算法截圖如下

然后他會呼叫rule.choose(key);方法,我們可以在進入方法前先看下IRule是啥,通過下圖我們可以很清楚的看到IRule里面所有的實作,之所以在這里提到IRule是因為IRule是Ribbon中實作負載均衡的一個很重要的規則,他實作了重置規則、輪詢規則、隨機規則及客戶端是否啟動輪詢的規則;在后面我看機會說其中一到兩種比較常用的演算法說明下

我們這里rule.choose(key);采用的是輪詢演算法,選擇PredicateBasedRule,進去后截圖如下

@Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer();
//根據我們的過濾規則過濾之后會根據輪詢去進行篩選,其中lb.getAllServers是獲取一個靜態的服務串列 Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); if (server.isPresent()) { return server.get(); } else { return null; } } }
我們進入chooseRoundRobinAfterFiltering,下面的輪詢比較簡單,他先把節點數量eligible.size()傳進去,然后通過incrementAndGetModulo方法獲取一個下標
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
//得到我們所有的配置資訊 List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
//配置數量 if (eligible.size() == 0) { return Optional.absent(); }
//進行輪詢計算 return Optional.of(eligible.get(incrementAndGetModulo(eligible.size()))); }
可以進入incrementAndGetModulo方法看下
private int incrementAndGetModulo(int modulo) { for (;;) {
//獲取下一個節點的當前值 int current = nextIndex.get();
//根據這個值進行取模運算 int next = (current + 1) % modulo;
//設定下一個值 if (nextIndex.compareAndSet(current, next) && current < modulo) return current; } }
上面就是輪詢演算法的實作,這個演算法的實作比較簡單,下面再來看一個隨機演算法的實作


public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } Server server = null; while (server == null) { if (Thread.interrupted()) { return null; } List<Server> upList = lb.getReachableServers();
//得到所有節點資訊 List<Server> allList = lb.getAllServers(); int serverCount = allList.size(); if (serverCount == 0) { /* * No servers. End regardless of pass, because subsequent passes * only get more restrictive. */ return null; } //傳入節點數量,然后隨機取值,如果有人想看怎么取的點擊這個chooseRandomInt就可以看到,它實作就一句話,就是把數量傳進去得到一個隨機值 int index = chooseRandomInt(serverCount); server = upList.get(index); if (server == null) { /* * The only time this should happen is if the server list were * somehow trimmed. This is a transient condition. Retry after * yielding. */ Thread.yield(); continue; } if (server.isAlive()) { return (server); } // Shouldn't actually happen.. but must be transient or a bug. server = null; Thread.yield(); } return server; }
隨機實作聊完后,再回到我們跟蹤的代碼 return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));通過演算法得到具體的節點后eligible.get就可以得到 對應下標的服務串列,這時就得到了什么localhost:8082的具體埠號了,這一步完成后其實Server server = getServer(loadBalancer, hint);的活就做完了,下面的活就是拿著具體埠去重構了,更新下時序圖


專案中所有例子原始碼:https://github.com/ljx958720/spring-cloud-Ribbon-1-.git
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/235261.html
標籤:其他
上一篇:Java基礎之:OOP——介面
