依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

代码

**需要feign生效,需要在配置类上加上@EnableFeignClients,**然后就是针对不同服务进行编码

1
2
3
4
5
6
7
8
9
10
11
12
//name这里可以自定义,但是最好为服务名(url默认为http://<name>),如果自定义name的话,则需要配置url,path可以理解为路径通用前缀
@FeignClient(name = "user-center", path="/user")
public interface UserCenterFeignClient {
@GetMapping("/getPort")
String getUserCenterPort();

@GetMapping("/findById/{id}")
UserDTO findById(@PathVariable("id") String id);

@PostMapping("/modifyBonusById")
UserDTO modifyBonusById(@RequestBody UserModifyBonusMsgDTO userModifyBonusMsgDTO);
}

@FeignClient本质就是通过feign.Feign类生成一个代理类,具体是通过feign.Feign#newInstance方法生成代理类

负载均衡

feign使用的负载均衡也是spring cloud commons下面的loadbalancer,具体参考RetryableFeignBlockingLoadBalancerClient类,RetryableFeignBlockingLoadBalancerClient实现了Client,但是Client不止有这一个实现类,只是这一个实现类整合了spring重试的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public Response execute(Request request, Request.Options options) throws IOException {
URI originalUri = URI.create(request.url());
String serviceId = originalUri.getHost();
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);
return (Response)retryTemplate.execute((context) -> {
Request feignRequest = null;
ServiceInstance retrievedServiceInstance = null;
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator.getSupportedLifecycleProcessors(this.loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class), RetryableRequestContext.class, ResponseData.class, ServiceInstance.class);
String hint = this.getHint(serviceId);
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest(new RetryableRequestContext((ServiceInstance)null, LoadBalancerUtils.buildRequestData(request), hint));
if (context instanceof LoadBalancedRetryContext) {
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context;
retrievedServiceInstance = lbContext.getServiceInstance();
if (retrievedServiceInstance == 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(serviceId, 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(Status.DISCARD, lbRequest, lbResponsex));
});
feignRequest = request;
} 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(request, reconstructedUrl);
}
}

org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(retrievedServiceInstance);
LoadBalancerProperties loadBalancerProperties = this.loadBalancerClientFactory.getProperties(serviceId);
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(serviceId, response, byteArray, URI.create(request.url()));
} else {
return response;
}
}, new LoadBalancedRecoveryCallback<Response, Response>() {
protected Response createResponse(Response response, URI uri) {
return response;
}
});
}

execute方法中retrievedServiceInstance = this.loadBalancerClient.choose(serviceId, lbRequest);这一行代码就是通过负载获取服务,其中loadBalancerClient就是spring cloud commons下面loadbalancer包中的类,org.springframework.cloud.client.loadbalancer.LoadBalancerClient,RestTemplate使用的也是这个类,所以feign和RestTemplate的负载逻辑差不多,既可以使用ribbon,也可以使用loadBalancer

更换http客户端

feign默认使用的是HttpUrlConnection进行http通信,但是feign也可以整合apache httpclient和okhttp,上面提到的RetryableFeignBlockingLoadBalancerClient有一个delegate变量,它就是feign的http客户端,从代码层面来讲,它和RetryableFeignBlockingLoadBalancerClient一样实现了Client,从某种意义上来讲,Client本身就是http客户端,只不过RetryableFeignBlockingLoadBalancerClient没有直接实现http客户端的功能,而是实现了其他的功能,通过delegate变量实现的http客户端,我认为这也是一种装饰模式,delegate默认情况下是feign.Client.Default类,而Default使用的就是HttpUrlConnection

整合apache httpclient需要引入依赖

依赖

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

配置

1
2
3
4
5
6
7
8
feign:
httpclient:
#设置feign访问方式
enabled: false
#feign最大连接数
max-connections: 200
#feign单个路劲最大连接数
max-connections-per-route: 50

通过引入依赖和配置,RetryableFeignBlockingLoadBalancerClient的delegate变成了feign.httpclient.ApacheHttpClient,而ApacheHttpClient使用的就是apache httpclient

如何生效的

具体生效主要是通过HttpClientFeignLoadBalancerConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({ApacheHttpClient.class})
@ConditionalOnBean({LoadBalancerClient.class, LoadBalancerClientFactory.class})
@ConditionalOnProperty(
value = {"feign.httpclient.enabled"},
matchIfMissing = true
)
@Conditional({HttpClient5DisabledConditions.class})
@Import({HttpClientFeignConfiguration.class})
@EnableConfigurationProperties({LoadBalancerClientsProperties.class})
class HttpClientFeignLoadBalancerConfiguration {
HttpClientFeignLoadBalancerConfiguration() {
}

@Bean
@ConditionalOnMissingBean
@Conditional({OnRetryNotEnabledCondition.class})
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient, LoadBalancerClientFactory loadBalancerClientFactory) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancerClientFactory);
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(
name = {"org.springframework.retry.support.RetryTemplate"}
)
@ConditionalOnBean({LoadBalancedRetryFactory.class})
@ConditionalOnProperty(
value = {"spring.cloud.loadbalancer.retry.enabled"},
havingValue = "true",
matchIfMissing = true
)
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory);
}
}

从代码中可以看出,feign.httpclient.enabled配置就是在HttpClientFeignLoadBalancerConfiguration类中起作用的,而RetryableFeignBlockingLoadBalancerClient的delegate变成ApacheHttpClient是因为HttpClientFeignLoadBalancerConfiguration向spring注册了一个新的RetryableFeignBlockingLoadBalancerClient,而这个RetryableFeignBlockingLoadBalancerClient的delegate被HttpClientFeignLoadBalancerConfiguration配置成了ApacheHttpClient,而ApacheHttpClient构造函数的参数就是apache httpclient的HttpClient类,这里ApacheHttpClient的HttpClient参数使用的是spring的bean,所以我们可以通过向spring注册一个新的apache HttpClient,达到我们想要的一些配置

自定义http客户端

参考上面的HttpClientFeignLoadBalancerConfiguration我们也可以自己配置使用何种http客户端,配置http客户端主要就是通过注册新的RetryableFeignBlockingLoadBalancerClient(甚至直接注册新的feign Client,但是这样会导致失去RetryableFeignBlockingLoadBalancerClient的功能),也需要解决两个问题,就是将feign的请求转为我们自己客户端的请求,并且将我们自己客户端的响应转为feign的响应,这两步我们暂时直接使用feign-httpclient的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
@Configuration
class HttpClientFeignLoadBalancerConfiguration {
HttpClientFeignLoadBalancerConfiguration() {
}

@Bean
@ConditionalOnMissingBean
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) {
MyHttpClient delegate = new MyHttpClient(HttpClients.createDefault());
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory, loadBalancerClientFactory);
}

public class MyHttpClient implements Client {

private final HttpClient httpClient;

public MyHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}

@Override
public Response execute(Request request, Request.Options options) throws IOException {
HttpUriRequest httpUriRequest = null;
try {
httpUriRequest = toHttpUriRequest(request, options);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
HttpResponse httpResponse = httpClient.execute(httpUriRequest);
return toFeignResponse(httpResponse, request);
}

HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws URISyntaxException {
RequestBuilder requestBuilder = RequestBuilder.create(request.httpMethod().name());

// per request timeouts
RequestConfig requestConfig =
(httpClient instanceof Configurable ? RequestConfig.copy(((Configurable) httpClient).getConfig())
: RequestConfig.custom())
.setConnectTimeout(options.connectTimeoutMillis())
.setSocketTimeout(options.readTimeoutMillis())
.build();
requestBuilder.setConfig(requestConfig);

URI uri = new URIBuilder(request.url()).build();

requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath());

// request query params
List<NameValuePair> queryParams =
URLEncodedUtils.parse(uri, requestBuilder.getCharset());
for (NameValuePair queryParam : queryParams) {
requestBuilder.addParameter(queryParam);
}

// request headers
boolean hasAcceptHeader = false;
for (Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) {
String headerName = headerEntry.getKey();
if (headerName.equalsIgnoreCase("Accept")) {
hasAcceptHeader = true;
}

if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) {
// The 'Content-Length' header is always set by the Apache client and it
// doesn't like us to set it as well.
continue;
}

for (String headerValue : headerEntry.getValue()) {
requestBuilder.addHeader(headerName, headerValue);
}
}
// some servers choke on the default accept string, so we'll set it to anything
if (!hasAcceptHeader) {
requestBuilder.addHeader("Accept", "*/*");
}

// request body
if (request.body() != null) {
HttpEntity entity = null;
if (request.charset() != null) {
ContentType contentType = getContentType(request);
String content = new String(request.body(), request.charset());
entity = new StringEntity(content, contentType);
} else {
entity = new ByteArrayEntity(request.body());
}

requestBuilder.setEntity(entity);
} else {
requestBuilder.setEntity(new ByteArrayEntity(new byte[0]));
}

return requestBuilder.build();
}

private ContentType getContentType(Request request) {
ContentType contentType = null;
for (Map.Entry<String, Collection<String>> entry : request.headers().entrySet())
if (entry.getKey().equalsIgnoreCase("Content-Type")) {
Collection<String> values = entry.getValue();
if (values != null && !values.isEmpty()) {
contentType = ContentType.parse(values.iterator().next());
if (contentType.getCharset() == null) {
contentType = contentType.withCharset(request.charset());
}
break;
}
}
return contentType;
}

Response toFeignResponse(HttpResponse httpResponse, Request request) throws IOException {
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();

String reason = statusLine.getReasonPhrase();

Map<String, Collection<String>> headers = new HashMap<String, Collection<String>>();
for (Header header : httpResponse.getAllHeaders()) {
String name = header.getName();
String value = header.getValue();

Collection<String> headerValues = headers.get(name);
if (headerValues == null) {
headerValues = new ArrayList<String>();
headers.put(name, headerValues);
}
headerValues.add(value);
}

return Response.builder()
.status(statusCode)
.reason(reason)
.headers(headers)
.request(request)
.body(toFeignBody(httpResponse))
.build();
}

Response.Body toFeignBody(HttpResponse httpResponse) {
final HttpEntity entity = httpResponse.getEntity();
if (entity == null) {
return null;
}
return new Response.Body() {

@Override
public Integer length() {
return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE
? (int) entity.getContentLength()
: null;
}

@Override
public boolean isRepeatable() {
return entity.isRepeatable();
}

@Override
public InputStream asInputStream() throws IOException {
return entity.getContent();
}

@SuppressWarnings("deprecation")
@Override
public Reader asReader() throws IOException {
return new InputStreamReader(asInputStream(), UTF_8);
}

@Override
public Reader asReader(Charset charset) throws IOException {
Util.checkNotNull(charset, "charset should not be null");
return new InputStreamReader(asInputStream(), charset);
}

@Override
public void close() throws IOException {
EntityUtils.consume(entity);
try {
EntityUtils.consume(entity);
} finally {
if (httpResponse instanceof CloseableHttpResponse)
((CloseableHttpResponse) httpResponse).close();
}
}
};
}

}
}

通过以上代码,就可以将我们自己的配置的RetryableFeignBlockingLoadBalancerClient注册到spring,并且feign就会使用我们配置的RetryableFeignBlockingLoadBalancerClient

拦截器

代码

最上面有提到,@FeignClient的本质就是feign.Feign生成代理类,而在生成代理类的时候,feign有一个配置拦截器的过程(具体参考feign.Feign类),feign拦截器需要实现feign.RequestInterceptor类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FeignHeaderInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
//下面三句话为springboot获取request的通用方法
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("X-Token");
if (token != null) {
requestTemplate.header("X-Token", token);
}
}
}

feign使拦截器生效有三种方式

全局配置

全局配置就是将拦截器注册到spring,这样的话,feign在构建任何一个feignclient的代理类时都会将这个拦截器配置进去,也就是对所有的feignclient都有效

1
2
3
4
5
6
7
8
@Configuration
public class FeignConfiguration {

@Bean
public RequestInterceptor feignHeaderInterceptor(){
return new FeignHeaderInterceptor();
}
}

局部配置

配置文件方式

spring cloud feign提供了配置文件的方式配置拦截器

1
2
3
4
5
6
7
feign:
client:
config:
user-center: #该项可填写@feignclient的name,若填写为default,则为全局配置
#配置feign拦截器
requestInterceptors:
- com.pengtong.contentcenter.feignclient.interceptor.FeignHeaderInterceptor

而配置文件的方式主要就是通过org.springframework.cloud.openfeign.FeignClientProperties类完成的,FeignClientProperties有一个config变量,config变量是一个Map<String, FeignClientConfiguration>对象,key就是配置的feign-name,FeignClientConfiguration就是feign的一些配置,所以这种方式不一定只是配置拦截器,具体参考org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration类,当key是“default”时,就是全局配置

代码方式

上面提到了@FeignClient注解,这个注解有一个configuration属性,这个属性就是对该feign进行一些配置,而具体的配置类就是使用@Bean对需要配置的属性进行设置,但是不使用@Configuration(不使用@Configuration代表不注册到spring),fegin会读取这个配置类中的使用了@Bean类进行配置,属于spring的一种机制。这里也不限制于拦截器,具体可以配置哪些属性可以参考FeignClientConfiguration类

1
@FeignClient(name = "user-center", configuration = {UserCenterConfig.class})
1
2
3
4
5
6
7
8
public class UserCenterConfig {

@Bean
public RequestInterceptor requestInterceptor(){
return new FeignHeaderInterceptor();
}

}

这种方式主要就是靠org.springframework.context.annotation.AnnotationConfigApplicationContext,它继承了org.springframework.beans.factory.BeanFactory,举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserCenterConfig {

@Bean
public RequestInterceptor requestInterceptor(){
return new FeignHeaderInterceptor();
}

public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(UserCenterConfig.class);
Map<String, RequestInterceptor> requestInterceptorMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, RequestInterceptor.class);
System.out.println(requestInterceptorMap);
}

}

本站总访问量:次本站总访问数:人
本站由 Peng Tong 使用 Stellar 1.29.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。