Spring Cloud feign 클라이언트 실행 프로세스 개요

67381 단어 Springcloud
개술
응용 프로그램이 초기화된 후에 개발자가 정의한 모든 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에 대응한다.dispatchMap이고 keyfeign 클라이언트의 방법이고 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여기LoadBalancerFeignClientSynchronousMethodHandler 구조 함수를 통해 전달된 것이다

  • 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 작업 원리

    좋은 웹페이지 즐겨찾기