【Feign】Feign重写Client,从服务注册中心动态获取服务,实现请求动态转发

对应场景:

  1. 服务注册中心(Nacos|Eureka)的服务个数是动态增减,服务是动态增加的。比如:消费服务A,消费服务B…,消费服务在Nacos上是动态增加的。
  2. 客户端调用,需要适配将请求转发到对应的消费服务上;

实现方式思路:

  1. feign接口配置中,在请求头(Headers)中添加需要转发到消费服务的名称;
  2. 在feignClient中,从请求头中获取到对应的服务名称,然后从服务注册中心检索到服务的ip和端口等信息,然后转发请求到对应的服务中。

feignClient重写代码如下:

  1. 请求feignClient
package feign;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
/**
 * feign client
 */
public class FeignCustomLoadBalancerClient implements Client {
    private static final Log LOG = LogFactory.getLog(FeignCustomLoadBalancerClient.class);

    private final Client delegate;

    private final LoadBalancerClient loadBalancerClient;

    private final LoadBalancerClientFactory loadBalancerClientFactory;

    private final String SERVICE_NAME_HEADER_KEY = "NACOS_SERVICE_NAME";


    public FeignCustomLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory) {
        this.delegate = delegate;
        this.loadBalancerClient = loadBalancerClient;
        this.loadBalancerClientFactory = loadBalancerClientFactory;
    }

    /**
     * 重写该方法=:支持动态从nacos上通过服务名称,获取对应服务信息
     */
    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        URI originalUri = URI.create(request.url());

        //从header中获取对应的服务名称
        Optional<String> optionalServiceId = request.headers().getOrDefault(SERVICE_NAME_HEADER_KEY, Collections.singletonList(originalUri.getHost())).stream().findFirst();

        String serviceId = null;
        if(optionalServiceId.isPresent()){
            serviceId = optionalServiceId.get();
            if(!serviceId.equals(originalUri.getHost())){
                request = buildRequest(request, UriComponentsBuilder.fromUri(originalUri).host(serviceId).toUriString());
            }
        }
        Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
        String hint = this.getHint(serviceId);
        DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(new RequestDataContext(LoadBalancerUtils.buildRequestData(request), hint));
        Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator.getSupportedLifecycleProcessors(this.loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), RequestDataContext.class, ResponseData.class, ServiceInstance.class);
        supportedLifecycleProcessors.forEach((lifecycle) -> {
            lifecycle.onStart(lbRequest);
        });
        //通过serviceId检索服务信息:一般情况下,是注册在服务注册中心的对应服务名称(e.g:nacos|eureka);
        ServiceInstance instance = this.loadBalancerClient.choose(serviceId, lbRequest);
        org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(instance);
        String message;
        if (instance == null) {
            message = "Load balancer does not contain an instance for the service " + serviceId;
            if (LOG.isWarnEnabled()) {
                LOG.warn(message);
            }

            supportedLifecycleProcessors.forEach((lifecycle) -> {
                lifecycle.onComplete(new CompletionContext(CompletionContext.Status.DISCARD, lbRequest, lbResponse));
            });
            return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value()).body(message, StandardCharsets.UTF_8).build();
        } else {
            message = this.loadBalancerClient.reconstructURI(instance, originalUri).toString();
            Request newRequest = this.buildRequest(request, message);
            LoadBalancerProperties loadBalancerProperties = this.loadBalancerClientFactory.getProperties(serviceId);
            return LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(this.delegate, options, newRequest, lbRequest, lbResponse, supportedLifecycleProcessors, loadBalancerProperties.isUseRawStatusCodeInResponseData());
        }
    }

    protected Request buildRequest(Request request, String reconstructedUrl) {
        return Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(), request.charset(), request.requestTemplate());
    }

    public Client getDelegate() {
        return this.delegate;
    }

    private String getHint(String serviceId) {
        LoadBalancerProperties properties = this.loadBalancerClientFactory.getProperties(serviceId);
        String defaultHint = (String) properties.getHint().getOrDefault("default", "default");
        String hintPropertyValue = (String) properties.getHint().get(serviceId);
        return hintPropertyValue != null ? hintPropertyValue : defaultHint;
    }
}

  1. 重试RetryClient
package feign;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.loadbalancer.LoadBalancerResponseStatusCodeException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.retry.RetryListener;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.NoBackOffPolicy;
import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.net.URI;
import java.util.*;

public class RetryFeignCustomLoadBalancerClient implements Client{
    private static final Log LOG = LogFactory.getLog(RetryFeignCustomLoadBalancerClient.class);
    private final Client delegate;
    private final LoadBalancerClient loadBalancerClient;
    private final LoadBalancedRetryFactory loadBalancedRetryFactory;
    private final LoadBalancerClientFactory loadBalancerClientFactory;

    private final String SERVICE_NAME_HEADER_KEY = "NACOS_SERVICE_NAME";

    /** @deprecated */
    @Deprecated
    public RetryFeignCustomLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
        this.delegate = delegate;
        this.loadBalancerClient = loadBalancerClient;
        this.loadBalancedRetryFactory = loadBalancedRetryFactory;
        this.loadBalancerClientFactory = loadBalancerClientFactory;
    }

    public RetryFeignCustomLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) {
        this.delegate = delegate;
        this.loadBalancerClient = loadBalancerClient;
        this.loadBalancedRetryFactory = loadBalancedRetryFactory;
        this.loadBalancerClientFactory = loadBalancerClientFactory;
    }

    public Response execute(Request request, Request.Options options) throws IOException {
        URI originalUri = URI.create(request.url());

        //从header中获取对应的服务名称
        Optional<String> optionalServiceId = request.headers().getOrDefault(SERVICE_NAME_HEADER_KEY, Collections.singletonList(originalUri.getHost())).stream().findFirst();

        String serviceId = null;
        if(optionalServiceId.isPresent()){
            serviceId = optionalServiceId.get();
            if(!serviceId.equals(originalUri.getHost())){
                request = buildRequest(request, UriComponentsBuilder.fromUri(originalUri).host(serviceId).toUriString());
            }
        }

        Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
        LoadBalancedRetryPolicy retryPolicy = this.loadBalancedRetryFactory.createRetryPolicy(serviceId, this.loadBalancerClient);
        RetryTemplate retryTemplate = this.buildRetryTemplate(serviceId, request, retryPolicy);

        String finalServiceId = serviceId;
        Request finalRequest = request;
        return (Response)retryTemplate.execute((context) -> {
            Request feignRequest = null;
            ServiceInstance retrievedServiceInstance = null;
            Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator.getSupportedLifecycleProcessors(this.loadBalancerClientFactory.getInstances(finalServiceId, LoadBalancerLifecycle.class), RetryableRequestContext.class, ResponseData.class, ServiceInstance.class);
            String hint = this.getHint(finalServiceId);
            DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(new RetryableRequestContext((ServiceInstance)null, LoadBalancerUtils.buildRequestData(feignRequest), hint));
            if (context instanceof LoadBalancedRetryContext) {
                LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context;
                ServiceInstance serviceInstance = lbContext.getServiceInstance();
                if (serviceInstance == null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Service instance retrieved from LoadBalancedRetryContext: was null. Reattempting service instance selection");
                    }

                    ServiceInstance previousServiceInstance = lbContext.getPreviousServiceInstance();
                    ((RetryableRequestContext)lbRequest.getContext()).setPreviousServiceInstance(previousServiceInstance);
                    supportedLifecycleProcessors.forEach((lifecycle) -> {
                        lifecycle.onStart(lbRequest);
                    });
                    retrievedServiceInstance = this.loadBalancerClient.choose(finalServiceId, lbRequest);
                    if (LOG.isDebugEnabled()) {
                        LOG.debug(String.format("Selected service instance: %s", retrievedServiceInstance));
                    }

                    lbContext.setServiceInstance(retrievedServiceInstance);
                }

                if (retrievedServiceInstance == null) {
                    if (LOG.isWarnEnabled()) {
                        LOG.warn("Service instance was not resolved, executing the original request");
                    }

                    org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponsex = new DefaultResponse(retrievedServiceInstance);
                    supportedLifecycleProcessors.forEach((lifecycle) -> {
                        lifecycle.onComplete(new CompletionContext(CompletionContext.Status.DISCARD, lbRequest, lbResponsex));
                    });
                    feignRequest = finalRequest;
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug(String.format("Using service instance from LoadBalancedRetryContext: %s", retrievedServiceInstance));
                    }

                    String reconstructedUrl = this.loadBalancerClient.reconstructURI(retrievedServiceInstance, originalUri).toString();
                    feignRequest = this.buildRequest(finalRequest, reconstructedUrl);
                }
            }

            org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(retrievedServiceInstance);
            LoadBalancerProperties loadBalancerProperties = this.loadBalancerClientFactory.getProperties(finalServiceId);
            Response response = LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(this.delegate, options, feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors, retrievedServiceInstance != null, loadBalancerProperties.isUseRawStatusCodeInResponseData());
            int responseStatus = response.status();
            if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug(String.format("Retrying on status code: %d", responseStatus));
                }

                byte[] byteArray = response.body() == null ? new byte[0] : StreamUtils.copyToByteArray(response.body().asInputStream());
                response.close();
                throw new LoadBalancerResponseStatusCodeException(finalServiceId, response, byteArray, URI.create(finalRequest.url()));
            } else {
                return response;
            }
        }, new LoadBalancedRecoveryCallback<Response, Response>() {
            protected Response createResponse(Response response, URI uri) {
                return response;
            }
        });
    }

    protected Request buildRequest(Request request, String reconstructedUrl) {
        return Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(), request.charset(), request.requestTemplate());
    }

    private RetryTemplate buildRetryTemplate(String serviceId, Request request, LoadBalancedRetryPolicy retryPolicy) {
        RetryTemplate retryTemplate = new RetryTemplate();
        BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory.createBackOffPolicy(serviceId);
        retryTemplate.setBackOffPolicy((BackOffPolicy)(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy));
        RetryListener[] retryListeners = this.loadBalancedRetryFactory.createRetryListeners(serviceId);
        if (retryListeners != null && retryListeners.length != 0) {
            retryTemplate.setListeners(retryListeners);
        }

        retryTemplate.setRetryPolicy((RetryPolicy)(retryPolicy == null ? new NeverRetryPolicy() : new InterceptorRetryPolicy(this.toHttpRequest(request), retryPolicy, this.loadBalancerClient, serviceId)));
        return retryTemplate;
    }

    public Client getDelegate() {
        return this.delegate;
    }

    private HttpRequest toHttpRequest(Request request) {
        return new HttpRequest() {
            public HttpMethod getMethod() {
                return HttpMethod.resolve(request.httpMethod().name());
            }

            public String getMethodValue() {
                return this.getMethod().name();
            }

            public URI getURI() {
                return URI.create(request.url());
            }

            public HttpHeaders getHeaders() {
                Map<String, List<String>> headers = new HashMap();
                Map<String, Collection<String>> feignHeaders = request.headers();
                Iterator var3 = feignHeaders.keySet().iterator();

                while(var3.hasNext()) {
                    String key = (String)var3.next();
                    headers.put(key, new ArrayList((Collection)feignHeaders.get(key)));
                }

                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(headers);
                return httpHeaders;
            }
        };
    }

    private String getHint(String serviceId) {
        LoadBalancerProperties properties = this.loadBalancerClientFactory.getProperties(serviceId);
        String defaultHint = (String)properties.getHint().getOrDefault("default", "default");
        String hintPropertyValue = (String)properties.getHint().get(serviceId);
        return hintPropertyValue != null ? hintPropertyValue : defaultHint;
    }
}

  1. 负载工具类
package feign;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.RequestData;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.util.MultiValueMap;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

public class LoadBalancerUtils {

    private LoadBalancerUtils() {
        throw new IllegalStateException("Can't instantiate a utility class");
    }

    static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient,
                                                               Request.Options options,
                                                               Request feignRequest,
                                                               org.springframework.cloud.client.loadbalancer.Request lbRequest,
                                                               org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
                                                               Set<LoadBalancerLifecycle> supportedLifecycleProcessors,
                                                               boolean loadBalanced,
                                                               boolean useRawStatusCodes) throws IOException {
        supportedLifecycleProcessors.forEach((lifecycle) -> {
            lifecycle.onStartRequest(lbRequest, lbResponse);
        });

        try {
            Response response = feignClient.execute(feignRequest, options);
            if (loadBalanced) {
                supportedLifecycleProcessors.forEach((lifecycle) -> {
                    lifecycle.onComplete(new CompletionContext(CompletionContext.Status.SUCCESS, lbRequest, lbResponse, buildResponseData(response)));
                });
            }

            return response;
        } catch (Exception var9) {
            if (loadBalanced) {
                supportedLifecycleProcessors.forEach((lifecycle) -> {
                    lifecycle.onComplete(new CompletionContext(CompletionContext.Status.FAILED, var9, lbRequest, lbResponse));
                });
            }

            throw var9;
        }
    }

    static ResponseData buildResponseData(Response response) {
        HttpHeaders responseHeaders = new HttpHeaders();
        response.headers().forEach((key, value) -> {
            responseHeaders.put(key, new ArrayList(value));
        });
        return  new ResponseData(HttpStatus.resolve(response.status()),responseHeaders, null,buildRequestData(response.request()));
    }

    static RequestData buildRequestData(Request request) {
        HttpHeaders requestHeaders = new HttpHeaders();
        request.headers().forEach((key, value) -> {
            requestHeaders.put(key, new ArrayList(value));
        });
        return new RequestData(HttpMethod.resolve(request.httpMethod().name()), URI.create(request.url()), requestHeaders, (MultiValueMap)null, new HashMap());
    }

    static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient,
                                                               Request.Options options,
                                                               Request feignRequest,
                                                               org.springframework.cloud.client.loadbalancer.Request lbRequest,
                                                               org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
                                                               Set<LoadBalancerLifecycle> supportedLifecycleProcessors,
                                                               boolean useRawStatusCodes) throws IOException {
        return executeWithLoadBalancerLifecycleProcessing(feignClient, options, feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors, true, useRawStatusCodes);
    }
}

  1. 将重写的Client 注入到IOC容器中;替代Feign默认的负载客户端;
package feign;

import feign.okhttp.OkHttpClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;

@Configuration
public class FeignCustomLoadBalancerConfiguration {

    @ConditionalOnClass(OkHttpClient.class)
    @ConditionalOnProperty("feign.okhttp.enabled")
    @Configuration(proxyBeanMethods = false)
    @Import(OkHttpFeignConfiguration.class)
    static class OkHttpConfiguration{
        @Bean
        @Primary
        @ConditionalOnProperty(
                value = {"spring.cloud.loadbalancer.retry.enabled"},
                havingValue = "false"
        )
        public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory) {
            OkHttpClient delegate = new OkHttpClient(okHttpClient);
            return new FeignCustomLoadBalancerClient(delegate, loadBalancerClient, loadBalancerClientFactory);
        }

        @Bean
        @Primary
        @ConditionalOnClass(
                name = {"org.springframework.retry.support.RetryTemplate"}
        )
        @ConditionalOnProperty(
                value = {"spring.cloud.loadbalancer.retry.enabled"},
                havingValue = "true",
                matchIfMissing = true
        )
        public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) {
            OkHttpClient delegate = new OkHttpClient(okHttpClient);
            return new RetryFeignCustomLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory);
        }
    }


    @EnableConfigurationProperties({LoadBalancerClientsProperties.class})
    @ConditionalOnProperty( value = "feign.okhttp.enabled",
            havingValue = "false",
            matchIfMissing = true
    )
    @Configuration(proxyBeanMethods = false)
    static class customDefaultFeignLoadBalancerConfiguration{

        @Bean
        @Primary
        @ConditionalOnProperty(
                value = {"spring.cloud.loadbalancer.retry.enabled"},
                havingValue = "false"
        )
        public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory) {
            return new FeignCustomLoadBalancerClient(new Client.Default(null,null), loadBalancerClient, loadBalancerClientFactory);
        }

        @Bean
        @Primary
        @ConditionalOnClass(
                name = {"org.springframework.retry.support.RetryTemplate"}
        )
        @ConditionalOnProperty(
                value = {"spring.cloud.loadbalancer.retry.enabled"},
                havingValue = "true",
                matchIfMissing = true
        )
        public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) {
            return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory);
        }

    }


}

  1. 在对应的Feign接口的配置中,Configuration或者全局的Intercepter中,在请求头中添加上述示例中的"NACOS_SERVICE_NAME"信息;

package feign;

import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignInterceptor implements RequestInterceptor{

    private final String SERVICE_NAME_HEADER_KEY = "NACOS_SERVICE_NAME";

    @Override
    public void apply(RequestTemplate requestTemplate) {
        //其他操作
        //在header中写入需要将请求转发到对应的服务的服务名称;
        requestTemplate.header(SERVICE_NAME_HEADER_KEY,"xxxx");
    }
}

你可能感兴趣的:(java,spring,cloud,eureka)