Spring Cloud feign 클라이언트 실행 프로세스 개요
응용 프로그램이 초기화된 후에 개발자가 정의한 모든
feign
클라이언트는 ReflectiveFeign$FeignInvocationHandler
실례에 대응한다.인스턴스를 구체적으로 생성하는 절차는 "@EnableFeignClients 작업 원리 주석"을 참조하십시오.본고는 단지 하나의 방법 요청이 발생했을 때 feign
클라이언트가 이를 어떻게 최종 원격 서비스로 전환하여 응답을 처리하는지를 분석하는데 다음과 같은 몇 가지 문제점이 있다.feign
방법 및 서비스의 대응1.
feign
방법 및 서비스의 대응실제로 초기화 과정에서
feign
클라이언트 실례를 만들었을 때 feign
방법과 서비스 대응이 완성되었고 최종적으로 생성된 실례에 포함된 정보를 보면 알 수 있다.예를 들어
feign
클라이언트 정의는 다음과 같습니다.package xxx;
import com.google.common.collect.Maps;
import xxx.TestModel;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "test-service", path = "/test")
public interface TestService {
@GetMapping(value = "/echo")
TestModel echoGet(@RequestParam("parameter") String parameter);
@PostMapping(value = "/echo/post",consumes = {"application/x-www-form-urlencoded"})
TestModel echoPostForm(Map<String, ?> formParams);
@PostMapping(value = "/echo/post")
TestModel echoPost(@RequestParam("parameter") String parameter);
}
결과적으로 생성된 클라이언트 인스턴스는 다음과 같습니다.
this = {ReflectiveFeign$FeignInvocationHandler@5249}
target = {Target$HardCodedTarget@5257}
"HardCodedTarget(type=TestService, name=test-service, url=http://test-service/test)"
dispatch = {LinkedHashMap@5258} size = 3
0 = {LinkedHashMap$Entry@5290} "public abstract xxx.TestModel xxx.TestService.echoPostForm(java.util.Map)"
1 = {LinkedHashMap$Entry@5291} "public abstract xxx.TestModel xxx.TestService.echoGet(java.lang.String)"
2 = {LinkedHashMap$Entry@5292} "public abstract xxx.TestModel xxx.TestService.echoPost(java.lang.String)"
여기
feign
클라이언트 실례의 속성target
은 목표 서비스http://test-service/test
를 가리키고 각feign
클라이언트 방법에 대한 설명도 더 진일보한 url
경로를 가리킨다. 예를 들어 방법echoGet
은 최종적으로 http://test-service/test/echo
에 대응하고 방법echoPostForm/echoPost
은 최종적으로 http://test-service/test/echo/post
에 대응한다.dispatch
는 Map
이고 key
는 feign
클라이언트의 방법이고 value
실현 유형은 SynchronousMethodHandler
이며 속성target
과 같은 정보와 대응 방법의 메타데이터를 포함한다.위의 정보는 클라이언트 방법이 호출되면
dispatch
에서 대응하는 방법SynchronousMethodHandler
을 호출한 것과 같습니다. // ReflectiveFeign$FeignInvocationHandler
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
// feign client dispatch SynchronousMethodHandler
return dispatch.get(method).invoke(args);
}
이를 통해 알 수 있듯이
feign
클라이언트의 특정한 서비스 방법이 호출될 때 대응하는 SynchronousMethodHandler#invoke
이 호출된다.SynchronousMethodHandler
는 목표 서비스를 포지셔닝하는 데 필요한 거의 모든 정보를 파악했다.부하 균형을 사용하면 test-service
어느 서비스 노드에 최종적으로 대응할지 아직 알 수 없다.2. 서비스 노드 선택
위 질문과 함께
SynchronousMethodHandler#invoke
접근 방식의 실행을 살펴보겠습니다.RequestTemplate
여기 구조RequestTemplate
는 하나RequestTemplate.Factory buildTemplateFromArgs
를 사용했다. 이 대상은 SynchronousMethodHandler
구조 함수를 통해 전달된 것으로 구조된 RequestTemplate
대상은 인터페이스 방법 정의에서 나온 path
속성, http method
속성, 파라미터 등을 포함하지만 목표 서비스 노드 정보를 포함하지 않는다.상기 예에서 언급한 세 가지 방법에 대해 생성된
RequestTemplate
의 요청은 다음과 같다.echoGet :
URL : http://test-service/test/echo?parameter=GET%20request
Body : []
echoPost :
URL : http://test-service/test/echo/post?parameter=POST%20request
Body : []
echoPostForm :
URL : http://test-service/test/echo/post
Content Type : application/x-www-form-urlencoded
Body : [parameter=POST+FORM+request]
#executeAndDecode
에 구축된 RequestTemplate
RequestInterceptor
와 응용 목표가 RestTemplate
로 하나의 Request
대상을 형성한다이때
RestTemplate
구체적인 서비스 노드를 제외한 모든 요청 정보를 가지고 있습니다.LoadBalancerFeignClient#execute
여기LoadBalancerFeignClient
는 SynchronousMethodHandler
구조 함수를 통해 전달된 것이다RetryableException
이상 시 재시도SynchronousMethodHandler#invoke
최종 호출LoadBalancerFeignClient#execute
:FeignLoadBalancer.RibbonRequest ribbonRequest
IClientConfig requestConfig
IClientConfig requestConfig = getClientConfig(options, clientName)
FeignLoadBalancer
(캐시 메모리 메커니즘 사용) FeignLoadBalancer
를 호출하는 방법executeWithLoadBalancer
FeignLoadBalancer#executeWithLoadBalancer
방법은 다음과 같다. public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig)
throws ClientException {
// LoadBalancerCommand
LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
try {
// ServerOperation LoadBalancerCommand
return command.submit(
new ServerOperation<T>() {
@Override
public Observable<T> call(Server server) {
// :
// server : localhost:8080
// request.uri : http://test/echo?parameter=value URL ,
//
// :
// URL finalUri :
// http://localhost:8080/test/echo?parameter=value
URI finalUri = reconstructURIWithServer(server, request.getUri());
S requestForServer = (S) request.replaceUri(finalUri);
try {
//
// FeignLoadBalancer ,GET/POST
return Observable.just(
AbstractLoadBalancerAwareClient.this.execute(
requestForServer, requestConfig));
}
catch (Exception e) {
return Observable.error(e);
}
}
})
.toBlocking()
.single();
} catch (Exception e) {
Throwable t = e.getCause();
if (t instanceof ClientException) {
throw (ClientException) t;
} else {
throw new ClientException(e);
}
}
}
위의 방법
FeignLoadBalancer#executeWithLoadBalancer
실현에서 #buildLoadBalancerCommand
방법을 통해 하나LoadBalancerCommand
를 구축했고 그의 #submit
방법은 여러 부하 균형 서비스 노드 중에서 하나를 선택하여 이번 호출에 사용할 수 있도록 한다. 구체적인 선택 논리 실현 방법LoadBalancerCommand#selectServer
에서.지금까지 우리는 어느 서비스 노드를 가장 중요하게 사용하는지 알았다.
다음은 최종 서비스 요청의 발기와 응답의 수신을 보겠습니다.이 부분의 논리는
FeignLoadBalancer#execute
에서
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(
override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout));
}
else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
Response response = request.client().execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
3. 서비스 요청 시작 및 응답 받기
위 코드에서 볼 수 있듯이 최종 요청은
Response response = request.client().execute(request.toRequest(), options)
를 통해 발동되고 응답을 받는다.이 부분은 feign.Client
인터페이스의 내부 클래스Default
에 구체적으로 구현되었다.package feign;
/**
* Submits HTTP Request requests. Implementations are expected to be thread-safe.
*/
public interface Client {
/**
* Executes a request against its Request#url() url and returns a response.
*
* @param request safe to replay.
* @param options options to apply to this request.
* @return connected response, Response.Body is absent or unread.
* @throws IOException on a network error connecting to Request#url().
*/
Response execute(Request request, Options options) throws IOException;
public static class Default implements Client {
private final SSLSocketFactory sslContextFactory;
private final HostnameVerifier hostnameVerifier;
/**
* Null parameters imply platform defaults.
*/
public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
this.sslContextFactory = sslContextFactory;
this.hostnameVerifier = hostnameVerifier;
}
@Override
public Response execute(Request request, Options options) throws IOException {
HttpURLConnection connection = convertAndSend(request, options);
return convertResponse(connection, request);
}
// `HTTP`
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final HttpURLConnection connection =
(HttpURLConnection) new URL(request.url()).openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(options.isFollowRedirects());
connection.setRequestMethod(request.httpMethod().name());
Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean gzipEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
boolean deflateEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);
boolean hasAcceptHeader = false;
Integer contentLength = null;
for (String field : request.headers().keySet()) {
if (field.equalsIgnoreCase("Accept")) {
hasAcceptHeader = true;
}
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
if (!gzipEncodedRequest && !deflateEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
}
}
// Some servers choke on the default accept string.
if (!hasAcceptHeader) {
connection.addRequestProperty("Accept", "*/*");
}
if (request.body() != null) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
} else if (deflateEncodedRequest) {
out = new DeflaterOutputStream(out);
}
try {
out.write(request.body());
} finally {
try {
out.close();
} catch (IOException suppressed) { // NOPMD
}
}
}
return connection;
}
//
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
int status = connection.getResponseCode();
String reason = connection.getResponseMessage();
if (status < 0) {
throw new IOException(format("Invalid status(%s) executing %s %s", status,
connection.getRequestMethod(), connection.getURL()));
}
Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>();
for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
// response message
if (field.getKey() != null) {
headers.put(field.getKey(), field.getValue());
}
}
Integer length = connection.getContentLength();
if (length == -1) {
length = null;
}
InputStream stream;
if (status >= 400) {
stream = connection.getErrorStream();
} else {
stream = connection.getInputStream();
}
return Response.builder()
.status(status)
.reason(reason)
.headers(headers)
.request(request)
.body(stream, length)
.build();
}
}
}
4. 서비스 응답 처리
절약하지 않은 경우
Spring Cloud
응용 프로그램feign
에서 클라이언트는 ResponseEntityDecoder
를 사용하여 서비스 응답을 해석한다.package org.springframework.cloud.openfeign.support;
/**
* Decoder adds compatibility for Spring MVC's ResponseEntity to any other decoder via
* composition.
* @author chadjaros
*/
public class ResponseEntityDecoder implements Decoder {
private Decoder decoder;
public ResponseEntityDecoder(Decoder decoder) {
this.decoder = decoder;
}
@Override
public Object decode(final Response response, Type type) throws IOException,
FeignException {
if (isParameterizeHttpEntity(type)) {
type = ((ParameterizedType) type).getActualTypeArguments()[0];
Object decodedObject = decoder.decode(response, type);
return createResponse(decodedObject, response);
}
else if (isHttpEntity(type)) {
return createResponse(null, response);
}
else {
return decoder.decode(response, type);
}
}
private boolean isParameterizeHttpEntity(Type type) {
if (type instanceof ParameterizedType) {
return isHttpEntity(((ParameterizedType) type).getRawType());
}
return false;
}
private boolean isHttpEntity(Type type) {
if (type instanceof Class) {
Class c = (Class) type;
return HttpEntity.class.isAssignableFrom(c);
}
return false;
}
@SuppressWarnings("unchecked")
private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
for (String key : response.headers().keySet()) {
headers.put(key, new LinkedList<>(response.headers().get(key)));
}
return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response
.status()));
}
}
참고 문장
주석 @EnableFeignClients 작업 원리
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
[MeU] Hashtag 기능 개발➡️ 기존 Tag 테이블에 존재하지 않는 해시태그라면 Tag , tagPostMapping 테이블에 모두 추가 ➡️ 기존에 존재하는 해시태그라면, tagPostMapping 테이블에만 추가 이후에 개발할 태그 기반 ...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.