今日教学:RestTemplate 结合 Ribbon 使用

193 阅读7分钟

Ribbon 是可以单独使用的,但是在 Spring Cloud 中使用 Ribbon 会更简单。因为 Spring Cloud 在 Ribbon 基础上进行了一层封装,将很多配置都集成好了。本文将在 Spring Cloud 中使用 Ribbon !

一、使用 RestTemplate 与整合 Ribbon

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

1. 使用 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(
        "http://localhost: 8081/house/ data?name="+name,HouseInfo.class);
}

@GetMapping("/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 地址,有两种方式,其中一种是字符串,另一种是 URL 形式。
  • 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(" /data")
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 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> 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-ribbon</artifactId>
</dependency>

这个配置我们加在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(
            "http://fsh-house/house/he11o", String.class);
        System.out.print1n("调用结果: " + result);
        return result ;
    }
}

测试步骤如下:

  1. 重启服务。
  2. 访问 http://localhost:8082/substitution/callHello 接口。
  3. 查看控制台输出,此时就知道负载有没有起作用了。

image.png

三、@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 注人拦截器,如下面代码所示。

@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(myLoadBalancerInterceptor());
                    restTemplate.setInterceptors(list);
                }
            }
        };
    }
}

维护一个 @MyLoadBalanced 的 RestTemplate 列表,在 SmartlnitializingSingleton 中对 RestTemplate 进行拦截器设置。

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

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

重启服务,访问 http://localhost:8082/substitutioncallHello 就可以看到控制台的输出了,这证明在接口调用的时候会进人该拦截器,输出如下:

image.png

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

首先看配置类,如何为 RestTemplate 设置拦截器,代码在 spring- cloud commonsjar 中的 orgspringframework.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);
                
                }
            }
        }
    };
}

下面看看拦截器的配置。可以知道,拦截器用的是LoadBalancerInterceptorRestTemplate 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<ClientHttpRequestInterceptor> list = new ArrayList<>(
                    restTemplate.getInterceptors());
                list.add(loadBalancer Interceptor);
                restTemplate. setInterceptors(list);
            }
        };
    }
}

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

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<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 != 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 方法来选择一个:

@Autowired
private 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: "http://localhost:8081"
}

五、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 :指定需要饥饿加载的服务名,也就是你需要调用的服务,若有多个则用逗号隔开。

怎么进行验证呢?网络情况确实不太好模拟,我们就通过日志输出来判断吧。先不配置 eager-load,在第一次调用的时候会有日志输出,内容如下:

image.png

在 fsh-house 的 client 初始化信息输出的基础上再加上 eager-load 的日志重启服务,就可以发现启动的时候会输出上面的日志,这就证明我们的 client 已经提前初始化好了。