如何在RestTemplate 结合 Ribbon 使用?

176 阅读7分钟

一、使用 RestTemplate 与整合 Ribbon Spring提供了一种简单便捷的模板类来进行API的调用,那就是 RestTemplate。

使用 RestTemplate 首先我们看看GET请求的使用方式:在 fsh-house 服务的 HouseController 中增加两个接口,一个通过 @RequestParam来传递参数,返回一个对象信息;另一个通过 @PathVarable来传递参数,返回一个字符串。尽量通过两个接口组装不同的形式,具体如下面代码所示。 @GetMapping("/data")public HouseInfo getData( @RequestParam("name") String name) {

return new HouseInfo(1L, "上海","虹口","东体小区"); } @GetMapping("/data/{name}") public String getData2(@PathVariable( "name") String name) {

return name; } 在 fsh-substitution 服务中用 RestTemplate来调用我们刚刚定义的两个接口,具体如下面代码所示。

@GetMapping("/data") public HouseInfo getData(@RequestParam("name") String name) {

return restTemplate.getForobject( "); }

@GetMapping("/data/{name}") public String getData2(@PathVariable("name") String name) { return restTemplate.getForobject(

"{name}",String.class,name); } 获取数据结果通过 RestTemplate的 getForObject 方法(具体如下面代码所示)来实现,此方法有三个重载的实现:

url:请求的 API 地址,有两种方式,其中一种是字符串,另一种是 URL 形式。 responseType :返回值的类型。 uriVariables : PathVariable 参数,有两种方式, 其中一种是可变参数,另一种是 Map 形式。 public T getForobject(String url, Class responseType,

object... uriVariables); public T getForobject (String url, Class responseType ,

Map<String, ?> uriVariables) ; public T getForobject(URI url, Class responseType) ; 除了 getForObject ,我们还可以使用 getForEntity 来获取数据,代码如下面代码所示。

@GetMapping(" /data")public HouseInfo getData(@RequestParam("name") String name) {

ResponseEntity responseEntity = restTemplate.getForEntity( "http:/ /localhost: 8081 /house/ data?name= "+name, HouseInfo.class) ; if (responseEntity.getStatusCodeValue() == 200) { return responseEntity.getBody(); } return nu1l ; } getForEntity 中可以获取返回的状态码、请求头等信息,通过 getBody 获取响应的内容。其余的和 getForObject 一样,也是有3个重载的实现。

接下来看看怎么使用POST方式调用接口。在 HouseController中增加一个save方法用来接收 HouseInfo数据,如下面代码所示。

@PostMapping("/save")public Long addData(@RequestBody HouseInfo houseInfo) {

System.out.println(houseInfo. getName()); return 1001L; } 接着写调用代码,用 postForObject来调用,如下面代码所示。

@GetMapping("/save")public Long add(){

HouseInfo houseInfo = new HouseInfo(); houseInfo.setCity("上海"); houseInfo.setRegion("虹口"); houseInfo.setName( "XXX"); Long id = restTemplate.postFor0bject( "http: //1ocalhost:8081/ house/save",houseInfo,Long.class); return id; } postForObject 同样有3个重载的实现。除了 postForObject 还可以使用 postForEntity 方法,用法都一样,如下面代码所示。

public T postForobject(String url, object request,

Class responseType, object... uriVariables); public T postForobject(String url, object request,

Class responseType, Map<String, ?> urivariables); public T postForobject(URI url, object request, Class responseType); 除了get和post对应的方法之外, RestTemplate 还提供了 put、 delete 等操作方法,还有一个比较实用的就是 exchange方法。 exchange 可以执行 get、 post、 put、 delete 这4种请求方式。

整合 Ribbon 在 Spring Cloud 项目中集成 Ribbon 只需要在 pom.xml 中加入下面的依赖即可,其实也可以不用配置,因为 Eureka 中已经引用了 Ribbon , 如下面代码所示。

org.springframework.cloud spring-cloud-starter-ribbon 这个配置我们加在 fangjia-fsh-substitution-service中。

二、RestTemplate 负载均衡示例 对之前的代码进行一些小改造,输出一些内容,证明我们集成的 Ribbon 是有效的。

改造 hello 接口,在接口中输出当前服务的端口,用来区分调用的服务,如下面代码所示。

@RestController@RequestMapping("/house" )public class HouseController {

@Value("${server .port}") private String serverPort;

@GetMapping("/hel1o") public String hel1o(){ return "Hello"+serverPort; } } 上述代码分别以8081和8083端口启动两个服务,后面要用到。

接着改造 callHello 接口的代码,将调用结果输出到控制台,如下面代码所示。

@RestController@RequestMapping("/substitution")public class Substitut ionController{

@Autowired private RestTemplate restTemplate; @GetMapping ("/cal1Hel1o") public String cal1Hello(){ String result = restTemplate. getFor0bject( "); System.out.print1n("调用结果: " + result); return result ; } } 测试步骤如下:

重启服务。 访问 接口。 查看控制台输出,此时就知道负载有没有起作用了。

三、@LoadBalanced 注解原理 相信大家一定有一个疑问:为什么在 RestTemplate 上加了一个 @LoadBalanced 之后, RestTemplate 就能够跟 Eureka 结合了,可以使用服务名称去调用接口,还可以负载均衡?

这功劳应归于Spring Cloud给我们做了大量的底层工作,因为它将这些都封装好了,我们用起来才会那么简单。框架就是为了简化代码,提高效率而产生的。

主要的逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行替换,或者根据具体的负载策略选择服务地址,然后再去调用,这就是 @LoadBalanced 的原理。

下面我们来实现一个简单的拦截器,看看在调用接口之前会不会进入这个拦截器。我们不做任何操作,就输出一句话,证明能进来就行了。如下面代码所示。

public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor{

@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); return execution.execute(request, body); } } 拦截器好了之后,我们再定义一个注解,复制 @LoadBalanced 的代码,改个名称就可以了,如下面代码所示。

@Target({ ElementType.FIELD,ElementType.PARAMETER,ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface MyLoadBalanced { } 然后定义一个配置类,来给 RestTemplate 注人拦截器,如下面代码所示。

@Configurationpublic class MyLoadBalancerAutoConfiguration{

@MyLoadBalanced @Autowired(required = false) private List 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 list = new ArrayList<>(restTemplate.getInterceptors()); list.add(myLoadBalancerInterceptor()); restTemplate.setInterceptors(list); } } }; } } 维护一个 @MyLoadBalanced 的 RestTemplate 列表,在 SmartlnitializingSingleton 中对 RestTemplate 进行拦截器设置。

然后改造我们之前的 RestTemplate 配置,将 @LoadBalanced 改成我们自定义的 @MyLoadBalanced,如下面代码所示。

@Bean//@LoadBalanced@MyLoadBalancedpublic RestTemplate getRestTemplate(){

return new RestTemplate() ; } 重启服务,访问 就可以看到控制台的输出了,这证明在接口调用的时候会进人该拦截器,输出如下:

通过这个小案例我们就能够清楚地知道 @LoadBalanced 的工作原理。接下来我们来看看源码中是怎样的一个逻辑。

首先看配置类,如何为 RestTemplate 设置拦截器,代码在 spring- cloud commonsjar 中的 orgspringframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration 类里面通过查看 LoadBalancerAutoConfiguration 的页游+源码,可以看到这里也是维护了一个 @LoadBalanced 的 RestTemplate 列表,如下面代码所示。

@LoadBalanced@Autowired(required = false)private List restTemplates = Collections.emptyList();@Beanpublic 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")static class LoadBalancerInterceptorConfig {

@Bean public LoadBalancerInterceptor ribbonInterceptor ( LoadBalancerClient loadBalancerClient , LoadBalancerRequestFactory requestFactory) return new LoadBalancer Interceptor(loadBalancerClient,requestFactory); }

@Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { List list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancer Interceptor); restTemplate. setInterceptors(list); } }; } }

如果相对electron有更多直观理解的, 也可以参考其官网:

www.sangpi.com

public class LoadBalancerInterceptor imp1ements

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 != nu11, "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 createRequest(final

HttpRequest request, final byte[] body,
    final ClientHttpRequestExecution execution) {

return new LoadBalancerRequest() { @Override public ClientHttpResponse apply(final ServiceInstance instance) throws Exception { HttpRequest serviceRequest = new ServiceRequestwrapper(request, instance, loadBalancer); if (transformers != nu11) { 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 获取对应的服务信息时,可以使用 LoadBalancer Client 来获取,比如你想获取一个 fsh-house 服务的服务地址,可以通过 LoadBalancerClient 的 choose 方法来选择一个:

@Autowiredprivate LoadBalancerClient loadBalancer;@GetMapping("/choose")public object chooseUrl() {

ServiceInstance instance = loadBalancer.choose("fsh-house"); return instance; } 访问接口,可以看到返回的信息如下:

{

serviceId: "fsh-house", 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: " } 五、Ribbon 饥饿加载 笔者从网上看到很多博客中都提到过的一个情况: 在进行服务调用的时候,如果网络情况不好,第一次调用会超时。有很多大神对此提出了解决方案,比如把超时时间改长一点、禁用超时等。 Spring Cloud 目前正在高速发展中,版本更新很快,我们能发现的问题基本上等新版本出来的时候就都修复了,或者提供了最优的解决方案。

这个超时的问题也是-样,Ribbon 的客户端是在第一次请求的时候初始化的,如果超时时间比较短的话,初始化 Client 的时间再加上请求接口的时间,就会导致第一次请求超时。

通过配置 eager-load 来提前初始化客户端就可以解决这个问题。

ribbon.eager-load.enabled = true ribbon eager-load.clients = fsh-house ribbon.eager-load.enabled :开启 Ribbon 的饥饿加载模式。 ribbon.eager-load.clients :指定需要饥饿加载的服务名,也就是你需要调用的服务,若有多个则用逗号隔开。