对应场景:
- 服务注册中心(Nacos|Eureka)的服务个数是动态增减,服务是动态增加的。比如:消费服务A,消费服务B…,消费服务在Nacos上是动态增加的。
- 客户端调用,需要适配将请求转发到对应的消费服务上;
实现方式思路:
- feign接口配置中,在请求头(Headers)中添加需要转发到消费服务的名称;
- 在feignClient中,从请求头中获取到对应的服务名称,然后从服务注册中心检索到服务的ip和端口等信息,然后转发请求到对应的服务中。
feignClient重写代码如下:
- 请求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;
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;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
URI originalUri = URI.create(request.url());
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);
});
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;
}
}
- 重试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
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());
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;
}
}
- 负载工具类
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);
}
}
- 将重写的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);
}
}
}
- 在对应的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) {
requestTemplate.header(SERVICE_NAME_HEADER_KEY,"xxxx");
}
}