如何使用LoadBalancerClient接口调用外部业务服务接口?

引言

Spring-Cloud-Ribbon作为微服务框架的负载均衡组件,默认使用RestTemplate接口调用外部服务接口。

原有项目中调用外部服务接口大多数采用的是使用了第三方的HttpClient库,如:Apache HttpClient或Asynchronous Http Client。

为了保证不影响现有业务接口的稳定性和无须修改原有业务接口代码,接下来本文会介绍如何改造。

原理分析

在使用Ribbon进行服务消费的时候,我们用到了RestTemplate,但是熟悉Spring的同学们是否产生过这样的疑问:RestTemplate不是Spring自己就有的吗?跟Ribbon的客户端负载均衡又有什么关系呢?我们来看RestTemplateRibbon是如何联系起来并实现客户端负载均衡的。

在实际的消费者示例中,我们只是简单的通过添加@LoadBalanced注解,那么它是如何实现客户端负载均衡的?仔细观察一下代码之前的代码,我们可以发现在消费者的例子中,可能就是这个注解@LoadBalanced是之前没有接触过的,并且从命名上来看也与负载均衡相关。我们不妨以此为线索来看看源码实现的机制。

@LoadBalanced注解源码的注释中,我们可以知道该注解用来给RestTemplate标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它。

通过搜索LoadBalancerClient,我们可以发现这是Spring Cloud中定义的一个接口:

public interface LoadBalancerClient extends ServiceInstanceChooser {
 
    /**
     * execute request using a ServiceInstance from the LoadBalancer for the specified
     * service
     * @param serviceId the service id to look up the LoadBalancer
     * @param request allows implementations to execute pre and post actions such as
     * incrementing metrics
     * @return the result of the LoadBalancerRequest callback on the selected
     * ServiceInstance
     */
     T execute(String serviceId, LoadBalancerRequest request) throws IOException;
 
    /**
     * execute request using a ServiceInstance from the LoadBalancer for the specified
     * service
     * @param serviceId the service id to look up the LoadBalancer
     * @param serviceInstance the service to execute the request to
     * @param request allows implementations to execute pre and post actions such as
     * incrementing metrics
     * @return the result of the LoadBalancerRequest callback on the selected
     * ServiceInstance
     */
     T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException;
 
    /**
     * Create a proper URI with a real host and port for systems to utilize.
     * Some systems use a URI with the logical serivce name as the host,
     * such as http://myservice/path/to/service.  This will replace the
     * service name with the host:port from the ServiceInstance.
     * @param instance
     * @param original a URI with the host as a logical service name
     * @return a reconstructed URI
     */
    URI reconstructURI(ServiceInstance instance, URI original);
}

从该接口中,我们可以通过定义的抽象方法来了解到客户端负载均衡器中应具备的几种能力:

  • ServiceInstance choose(String serviceId):根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。
  • T execute(String serviceId, LoadBalancerRequest request) throws IOException:使用从负载均衡器中挑选出的服务实例来执行请求内容。
  • T execute(String serviceId, ServiceInstance serviceInstance,LoadBalancerRequest request,) throws IOException:使用从负载均衡器中挑选出的服务实例来执行请求内容。
  • URI reconstructURI(ServiceInstance instance, URI original):为系统构建一个合适的“host:port”形式的URI。在分布式系统中,我们使用逻辑上的服务名称作为host来构建URI(替代服务实例的“host:port”形式)进行请求,比如:http://myservice/path/to/service。在该操作的定义中,前者ServiceInstance对象是带有host和port的具体服务实例,而后者URI对象则是使用逻辑服务名定义为host的URI,而返回的URI内容则是通过ServiceInstance的服务实例详情拼接出的具体“host:post”形式的请求地址。

顺着LoadBalancerClient接口的所属包org.springframework.cloud.client.loadbalancer,我们对其内容进行整理,可以得出如下类的关系图:

从类的命名上我们初步判断LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类。通过查看源码,我们可以验证这一点假设: 

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
 
    @LoadBalanced
    @Autowired(required = false)
    private List restTemplates = Collections.emptyList();
 
    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
            final List customizers) {
        return new SmartInitializingSingleton() {
            @Override
            public void afterSingletonsInstantiated() {
                for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                    for (RestTemplateCustomizer customizer : customizers) {
                        customizer.customize(restTemplate);
                    }
                }
            }
        };
    }
 
    @Autowired(required = false)
    private List transformers = Collections.emptyList();
 
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(
            LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
    }
 
    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    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 list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                }
            };
        }
    }
 
    @Configuration
    @ConditionalOnClass(RetryTemplate.class)
    public static class RetryAutoConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public RetryTemplate retryTemplate() {
            RetryTemplate template =  new RetryTemplate();
            template.setThrowLastExceptionOnExhausted(true);
            return template;
        }
 
        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
            return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();
        }
 
        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory() {
            return new LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory();
        }
 
        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryListenerFactory loadBalancedRetryListenerFactory() {
            return new LoadBalancedRetryListenerFactory.DefaultRetryListenerFactory();
        }
    }
 
    @Configuration
    @ConditionalOnClass(RetryTemplate.class)
    public static class RetryInterceptorAutoConfiguration {
        @Bean
        @ConditionalOnMissingBean
        public RetryLoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
                LoadBalancedRetryPolicyFactory lbRetryPolicyFactory,
                LoadBalancerRequestFactory requestFactory,
                LoadBalancedBackOffPolicyFactory backOffPolicyFactory,
                LoadBalancedRetryListenerFactory retryListenerFactory) {
            return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
                    lbRetryPolicyFactory, requestFactory, backOffPolicyFactory, retryListenerFactory);
        }
 
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
                final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
            return new RestTemplateCustomizer() {
                @Override
                public void customize(RestTemplate restTemplate) {
                    List list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                }
            };
        }
    }
}

LoadBalancerAutoConfiguration类头上的注解可以知道Ribbon实现的负载均衡自动化配置需要满足下面两个条件:

  • @ConditionalOnClass(RestTemplate.class)RestTemplate类必须存在于当前工程的环境中。
  • @ConditionalOnBean(LoadBalancerClient.class):在Spring的Bean工程中有必须有LoadBalancerClient的实现Bean。

在该自动化配置类中,主要做了下面几件事:

  • 创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
  • 创建了一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
  • 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
  • 创建了一个RestTemplate的Bean,用于实现对客户发起的请求进行拦截,以实现客户端重试机制。
  • 创建了一个LoadBalancedRetryPolicyFactory的工厂对象,用于创建负载均衡重试策略。
  • 创建了一个LoadBalancedBackOffPolicyFactory的工厂对象,用于负载均衡回退策略。
  • 创建了一个LoadBalancedRetryListenerFactory的工厂对象,用于负载均衡重试监听回调。
  • 创建了一个RetryLoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端重试。

接下来,我们看看LoadBalancerInterceptor拦截器是如何将一个普通的RestTemplate变成客户端负载均衡的:

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) {
        // for backwards compatibility
        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));
    }
}

通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient的实现。当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept函数所拦截。

由于我们在使用RestTemplate时候采用了服务名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的请求,具体的实现代码如下:

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));
}

通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient的实现。当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept函数所拦截。

由于我们在使用RestTemplate时候采用了服务名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的请求,具体的实现代码如下:

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));

}

分析到这里,LoadBalancerClient还只是一个抽象的负载均衡器接口,所以我们还需要找到它的具体实现类来进一步分析。通过查看ribbon的源码,我们可以很容易的在org.springframework.cloud.netflix.ribbon包下找到对应的实现类:RibbonLoadBalancerClient

public class RibbonLoadBalancerClient implements LoadBalancerClient {



    private SpringClientFactory clientFactory;



    public RibbonLoadBalancerClient(SpringClientFactory clientFactory) {

        this.clientFactory = clientFactory;

    }



    @Override

    public URI reconstructURI(ServiceInstance instance, URI original) {

        Assert.notNull(instance, "instance can not be null");

        String serviceId = instance.getServiceId();

        RibbonLoadBalancerContext context = this.clientFactory

                .getLoadBalancerContext(serviceId);

        Server server = new Server(instance.getHost(), instance.getPort());

        IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);

        ServerIntrospector serverIntrospector = serverIntrospector(serviceId);

        URI uri = RibbonUtils.updateToHttpsIfNeeded(original, clientConfig,

                serverIntrospector, server);

        return context.reconstructURIWithServer(server, uri);

    }



    @Override

    public ServiceInstance choose(String serviceId) {

        Server server = getServer(serviceId);

        if (server == null) {

            return null;

        }

        return new RibbonServer(serviceId, server, isSecure(server, serviceId),

                serverIntrospector(serviceId).getMetadata(server));

    }



    @Override

    public  T execute(String serviceId, LoadBalancerRequest request) throws IOException {

        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);

        Server server = getServer(loadBalancer);

        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);

    }



    @Override

    public  T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException {

        Server server = null;

        if(serviceInstance instanceof RibbonServer) {

            server = ((RibbonServer)serviceInstance).getServer();

        }

        if (server == null) {

            throw new IllegalStateException("No instances available for " + serviceId);

        }



        RibbonLoadBalancerContext context = this.clientFactory

                .getLoadBalancerContext(serviceId);

        RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);



        try {

            T returnVal = request.apply(serviceInstance);

            statsRecorder.recordStats(returnVal);

            return returnVal;

        }

        // catch IOException and rethrow so RestTemplate behaves correctly

        catch (IOException ex) {

            statsRecorder.recordStats(ex);

            throw ex;

        }

        catch (Exception ex) {

            statsRecorder.recordStats(ex);

            ReflectionUtils.rethrowRuntimeException(ex);

        }

        return null;

    }



    private ServerIntrospector serverIntrospector(String serviceId) {

        ServerIntrospector serverIntrospector = this.clientFactory.getInstance(serviceId,

                ServerIntrospector.class);

        if (serverIntrospector == null) {

            serverIntrospector = new DefaultServerIntrospector();

        }

        return serverIntrospector;

    }



    private boolean isSecure(Server server, String serviceId) {

        IClientConfig config = this.clientFactory.getClientConfig(serviceId);

        ServerIntrospector serverIntrospector = serverIntrospector(serviceId);

        return RibbonUtils.isSecure(config, serverIntrospector, server);

    }



    protected Server getServer(String serviceId) {

        return getServer(getLoadBalancer(serviceId));

    }



    protected Server getServer(ILoadBalancer loadBalancer) {

        if (loadBalancer == null) {

            return null;

        }

        return loadBalancer.chooseServer("default"); // TODO: better handling of key

    }



    protected ILoadBalancer getLoadBalancer(String serviceId) {

        return this.clientFactory.getLoadBalancer(serviceId);

    }



    public static class RibbonServer implements ServiceInstance {

        private final String serviceId;

        private final Server server;

        private final boolean secure;

        private Map metadata;



        public RibbonServer(String serviceId, Server server) {

            this(serviceId, server, false, Collections. emptyMap());

        }



        public RibbonServer(String serviceId, Server server, boolean secure,

                Map metadata) {

            this.serviceId = serviceId;

            this.server = server;

            this.secure = secure;

            this.metadata = metadata;

        }



        @Override

        public String getServiceId() {

            return this.serviceId;

        }



        @Override

        public String getHost() {

            return this.server.getHost();

        }



        @Override

        public int getPort() {

            return this.server.getPort();

        }



        @Override

        public boolean isSecure() {

            return this.secure;

        }



        @Override

        public URI getUri() {

            return DefaultServiceInstance.getUri(this);

        }



        @Override

        public Map getMetadata() {

            return this.metadata;

        }



        public Server getServer() {

            return this.server;

        }



        @Override

        public String toString() {

            final StringBuffer sb = new StringBuffer("RibbonServer{");

            sb.append("serviceId='").append(serviceId).append('\'');

            sb.append(", server=").append(server);

            sb.append(", secure=").append(secure);

            sb.append(", metadata=").append(metadata);

            sb.append('}');

            return sb.toString();

        }

    }



}

可以看到,在execute函数的实现中,第一步做的就是通过传入的服务名serviceId去获得具体的服务实例:

@Override

public  T execute(String serviceId, LoadBalancerRequest request) throws IOException {

    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);

    Server server = getServer(loadBalancer);

    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);

}

通过getServer函数的实现源码,我们可以看到这里获取具体服务实例的时候并没有使用LoadBalancerClient接口中的choose函数,而是使用了ribbon自身的ILoadBalancer接口中定义的chooseServer函数。


protected Server getServer(ILoadBalancer loadBalancer) {

    if (loadBalancer == null) {

        return null;

    }

    return loadBalancer.chooseServer("default"); // TODO: better handling of key

}

为了搞清楚具体的逻辑,先来认识一下ILoadBalancer接口:

public interface ILoadBalancer {



    /**

     * Initial list of servers.

     * This API also serves to add additional ones at a later time

     * The same logical server (host:port) could essentially be added multiple times

     * (helpful in cases where you want to give more "weightage" perhaps ..)

     *

     * @param newServers new servers to add

     */

    public void addServers(List newServers);

     

    /**

     * Choose a server from load balancer.

     *

     * @param key An object that the load balancer may use to determine which server to return. null if

     *         the load balancer does not use this parameter.

     * @return server chosen

     */

    public Server chooseServer(Object key);

     

    /**

     * To be called by the clients of the load balancer to notify that a Server is down

     * else, the LB will think its still Alive until the next Ping cycle - potentially

     * (assuming that the LB Impl does a ping)

     *

     * @param server Server to mark as down

     */

    public void markServerDown(Server server);

     

    /**

     * @deprecated 2016-01-20 This method is deprecated in favor of the

     * cleaner {@link #getReachableServers} (equivalent to availableOnly=true)

     * and {@link #getAllServers} API (equivalent to availableOnly=false).

     *

     * Get the current list of servers.

     *

     * @param availableOnly if true, only live and available servers should be returned

     */

    @Deprecated

    public List getServerList(boolean availableOnly);



    /**

     * @return Only the servers that are up and reachable.

     */

    public List getReachableServers();



    /**

     * @return All known servers, both reachable and unreachable.

     */

    public List getAllServers();

}

可以看到,在该接口中定义了一个软负载均衡器需要的一系列抽象操作(未例举过期函数):

  • addServers:向负载均衡器中维护的实例列表增加服务实例。
  • chooseServer:通过某种策略,从负载均衡器中挑选出一个具体的服务实例。
  • markServerDown:用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常服务的。
  • getReachableServers:获取当前正常服务的实例列表。
  • getAllServers:获取所有已知的服务实例列表,包括正常服务和停止服务的实例。

在该接口定义中涉及到的Server对象定义的是一个传统的服务端节点,在该类中存储了服务端节点的一些元数据信息,包括:host、port、zone、schema以及一些部署信息等。

而对于该接口的实现,我们可以整理出如上图所示的结构。我们可以看到BaseLoadBalancer类实现了基础的负载均衡,而DynamicServerListLoadBalancerZoneAwareLoadBalancer在负载均衡的策略上做了一些功能的扩展。

那么在整合Ribbon的时候Spring Cloud默认采用了哪个具体实现呢?我们通过spring-cloud-netflix-core包的RibbonClientConfiguration配置类,可以知道在整合时默认采用了ZoneAwareLoadBalancer来实现负载均衡器。

@Bean

@ConditionalOnMissingBean

public ILoadBalancer ribbonLoadBalancer(IClientConfig config,

        ServerList serverList, ServerListFilter serverListFilter,

        IRule rule, IPing ping, ServerListUpdater serverListUpdater) {

    if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {

        return this.propertiesFactory.get(ILoadBalancer.class, config, name);

    }

    return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,

            serverListFilter, serverListUpdater);

}

下面,我们再回到RibbonLoadBalancerClientexecute函数逻辑,在通过ZoneAwareLoadBalancerchooseServer函数获取了负载均衡策略分配到的服务实例对象Server之后,将其内容包装成RibbonServer对象(该对象除了存储了服务实例的信息之外,还增加了服务名serviceId、是否需要使用HTTPS等其他信息)。然后使用该对象再回调LoadBalancerInterceptor请求拦截器中LoadBalancerRequestapply(final ServiceInstance instance)函数,向一个实际的具体服务实例发起请求,从而实现一开始以服务名为host的URI请求,到实际访问host:post形式的具体地址的转换。

apply(final ServiceInstance instance)函数中传入的ServiceInstance接口是对服务实例的抽象定义。在该接口中暴露了服务治理系统中每个服务实例需要提供的一些基本信息,比如:serviceId、host、port等,具体定义如下:

public interface ServiceInstance {
 
    /**
     * @return the service id as registered.
     */
    String getServiceId();
 
    /**
     * @return the hostname of the registered ServiceInstance
     */
    String getHost();
 
    /**
     * @return the port of the registered ServiceInstance
     */
    int getPort();
 
    /**
     * @return if the port of the registered ServiceInstance is https or not
     */
    boolean isSecure();
 
    /**
     * @return the service uri address
     */
    URI getUri();
 
    /**
     * @return the key value pair metadata associated with the service instance
     */
    Map getMetadata();
}

而上面提到的具体包装Server服务实例的RibbonServer对象就是ServiceInstance接口的实现,可以看到它除了包含了Server对象之外,还存储了服务名、是否使用https标识以及一个Map类型的元数据集合。

public static class RibbonServer implements ServiceInstance {

    private final String serviceId;

    private final Server server;

    private final boolean secure;

    private Map metadata;

    public RibbonServer(String serviceId, Server server) {

        this(serviceId, server, false, Collections. emptyMap());

    }

    public RibbonServer(String serviceId, Server server, boolean secure,

            Map metadata) {

        this.serviceId = serviceId;

        this.server = server;

        this.secure = secure;

        this.metadata = metadata;
    }

}

那么apply(final ServiceInstance instance)函数,在接收到了具体ServiceInstance实例后,是如何通过LoadBalancerClient接口中的reconstructURI操作来组织具体请求地址的呢?

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 != null) {
                for (LoadBalancerRequestTransformer transformer : transformers) {
                    serviceRequest = transformer.transformRequest(serviceRequest, instance);
                }
            }
            return execution.execute(serviceRequest, body);
        }
 
    };
}

apply的实现中,我们可以看到它具体执行的时候,还传入了ServiceRequestWrapper对象,该对象继承了HttpRequestWrapper并重写了getURI函数,重写后的getURI会通过调用LoadBalancerClient接口的reconstructURI函数来重新构建一个URI来进行访问。

public class ServiceRequestWrapper extends HttpRequestWrapper {

    private final ServiceInstance instance;

    private final LoadBalancerClient loadBalancer;


    public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance,LoadBalancerClient loadBalancer) {

        super(request);

        this.instance = instance;

        this.loadBalancer = loadBalancer;

    }



    @Override

    public URI getURI() {

        URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());

        return uri;

    }

}

LoadBalancerInterceptor拦截器中,ClientHttpRequestExecution的实例具体执行execution.execute(serviceRequest, body)时,会调用InterceptingClientHttpRequestInterceptingRequestExecution类的execute函数,具体实现如下:

@Override

public ClientHttpResponse execute(HttpRequest request, final byte[] body) throws IOException {

    if (this.iterator.hasNext()) {

        ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();

        return nextInterceptor.intercept(request, body, this);

    }

    else {

        ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());

        for (Map.Entry> entry : request.getHeaders().entrySet()) {

            List values = entry.getValue();

            for (String value : values) {

                delegate.getHeaders().add(entry.getKey(), value);

            }

        }

        if (body.length > 0) {

            if (delegate instanceof StreamingHttpOutputMessage) {

                StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;

                streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {

                    @Override

                    public void writeTo(final OutputStream outputStream) throws IOException {

                        StreamUtils.copy(body, outputStream);

                    }

                });

            }

            else {

                StreamUtils.copy(body, delegate.getBody());

            }

        }

        return delegate.execute();

    }

}

可以看到在创建请求的时候requestFactory.createRequest(request.getURI(), request.getMethod());

这里request.getURI()会调用之前介绍的ServiceRequestWrapper对象中重写的getURI函数。此时,它就会使用RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的服务实例地址。

@Override

public URI reconstructURI(ServiceInstance instance, URI original) {

    Assert.notNull(instance, "instance can not be null");

    String serviceId = instance.getServiceId();

    RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);

    Server server = new Server(instance.getHost(), instance.getPort());

    IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);

    ServerIntrospector serverIntrospector = serverIntrospector(serviceId);

    URI uri = RibbonUtils.updateToHttpsIfNeeded(original, clientConfig,serverIntrospector, server);

    return context.reconstructURIWithServer(server, uri);

}

reconstructURI函数中,我们可以看到,它通过ServiceInstance实例对象的serviceId,从SpringClientFactory类的clientFactory对象中获取对应serviceId的负载均衡器的上下文RibbonLoadBalancerContext对象。然后根据ServiceInstance中的信息来构建具体服务实例信息的Server对象,并使用RibbonLoadBalancerContext对象的reconstructURIWithServer函数来构建服务实例的URI。

为了帮助理解,简单介绍一下上面提到的SpringClientFactoryRibbonLoadBalancerContext

  • SpringClientFactory类是一个用来创建客户端负载均衡器的工厂类,该工厂会为每一个不同名的ribbon客户端生成不同的Spring上下文。
  • RibbonLoadBalancerContext类是LoadBalancerContext的子类,该类用于存储一些被负载均衡器使用的上下文内容和Api操作(reconstructURIWithServer就是其中之一)。

reconstructURIWithServer的实现中我们可以看到,它同reconstructURI的定义类似。只是reconstructURI的第一个保存具体服务实例的参数使用了Spring Cloud定义的ServiceInstance,而reconstructURIWithServer中使用了Netflix中定义的Server,所以在RibbonLoadBalancerClient实现reconstructURI时候,做了一次转换,使用ServiceInstance的host和port信息来构建了一个Server对象来给reconstructURIWithServer使用。从reconstructURIWithServer的实现逻辑中,我们可以看到,它从Server对象中获取host和port信息,然后根据以服务名为host的URI对象original中获取其他请求信息,将两者内容进行拼接整合,形成最终要访问的服务实例的具体地址。

业务需求

由于原有业务服务接口中使用的是第三方HttpClient库来调用外部服务接口的,调用代码示例如下:

@Component
public class AccountApi {

	private static Logger log = LoggerFactory.getLogger(AccountApi.class);

	@Autowired
	private Setting setting;

	@Autowired
	private HttpComponent httpComponent;


	public User getUserBySsoid(Integer ssoid) {
		User user = new User();
		String url = MessageFormat.format(setting.API_User_Profile, String.valueOf(ssoid));
		log.info(MessageFormat.format("通过ssoid:[{0}] 获取用户信息,url:[{1}]", ssoid, url));
		try {
			Response response = httpComponent.syncHttpRequest(url, null, RequestMethod.GET);
			if (response.getStatusCode() != 200) {
				throw new Exception("查找用户信息接口出错,code: " + response.getStatusCode());
			}
			String data = response.getResponseBody();
			JSONObject object = JSONObject.parseObject(data);
			JSONObject userObject = object.getJSONObject("data");
			user = JSON.parseObject(userObject.toString(), User.class);
		} catch (Exception e) {
			log.error(MessageFormat.format("通过ssoid:[{0}] 获取用户信息出错,url:[{1}],错误信息:{2}", ssoid, url, e.toString()));
		}
		reporter.report(Span.builder().name(url).build());
		return user;
	}

 

解决方案

那如何进行改造呢?其实了解了Ribbon负载均衡组件的基本原理后,我们知道原有业务代码中最核心要解决的就是请求URL地址的转换问题,RibbonLoadBalancerClient最终会将逻辑地址(service-id)转换为真实地址+端口号将请求发送出去,所以我们要做的就是将原有的URL地址根据规则进行解析出service-id,然后根据service-id从注册中心中获取真实的主机名和端口号即可,示例如下:

private String reconstructURL(String original) {
	Assert.notNull(original, "original can not be null");
	StringBuilder builder = new StringBuilder("http://");
	String uri1 = original.substring(original.indexOf("//")+2);
	String uri2 = uri1.substring(uri1.indexOf("/"));
	String[] originals = uri1.split("/");
	ServiceInstance instance = lbClient.choose(originals[0]);
	Server server = new Server(instance.getHost(), instance.getPort());
	return builder.append(server.getHostPort()).append(uri2).toString();
}

 reconstructURL函数就是根据源URL按照一定的规则进行解析后重新生成新的URL地址,改造后的代码如下所示:

public User getUserBySsoid(Integer ssoid) {
	String requestUrl = reconstructURL(setting.API_User_Profile);
	User user = new User();
	String url = MessageFormat.format(requestUrl, String.valueOf(ssoid));
	log.info(MessageFormat.format("通过ssoid:[{0}] 获取用户信息,url:[{1}]", ssoid, url));
	try {
		Response response = httpComponent.syncHttpRequest(url, null, RequestMethod.GET);
		if (response.getStatusCode() != 200) {
			throw new Exception("查找用户信息接口出错,code: " + response.getStatusCode());
		}
		String data = response.getResponseBody();
		JSONObject object = JSONObject.parseObject(data);
		JSONObject userObject = object.getJSONObject("data");
		user = JSON.parseObject(userObject.toString(), User.class);
	} catch (Exception e) {
		log.error(MessageFormat.format("通过ssoid:[{0}] 获取用户信息出错,url:[{1}],错误信息:{2}", ssoid, url, e.toString()));
	}
	reporter.report(Span.builder().name(url).build());
	return user;
}

上述只是具体的业务逻辑代码,在业务处理接口中我们需要通过@Autowired注解将LoadBalancerClient接口自动装配。

你可能感兴趣的:(SpringCloud)