content: package com.ctrip.framework.apollo.portal.component;
  
  import com.ctrip.framework.apollo.common.exception.ServiceException;
  import com.ctrip.framework.apollo.core.MetaDomainConsts;
  import com.ctrip.framework.apollo.core.dto.ServiceDTO;
  import com.ctrip.framework.apollo.core.enums.Env;
  import com.ctrip.framework.apollo.portal.constant.TracerEventType;
  import com.ctrip.framework.apollo.tracer.Tracer;
  import com.ctrip.framework.apollo.tracer.spi.Transaction;
  
  import org.apache.http.conn.ConnectTimeoutException;
  import org.apache.http.conn.HttpHostConnectException;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.core.ParameterizedTypeReference;
  import org.springframework.http.HttpMethod;
  import org.springframework.http.ResponseEntity;
  import org.springframework.stereotype.Component;
  import org.springframework.util.CollectionUtils;
  import org.springframework.web.client.RestClientException;
  import org.springframework.web.client.RestTemplate;
  import org.springframework.web.util.DefaultUriTemplateHandler;
  import org.springframework.web.util.UriTemplateHandler;
  
  import java.net.SocketTimeoutException;
  import java.util.List;
  
  import javax.annotation.PostConstruct;
  
  /**
   * 封装RestTemplate. admin server集群在某些机器宕机或者超时的情况下轮询重试
   */
  @Component
  public class RetryableRestTemplate {
  
    private Logger logger = LoggerFactory.getLogger(RetryableRestTemplate.class);
  
    private UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler();
  
    private RestTemplate restTemplate;
  
    @Autowired
    private RestTemplateFactory restTemplateFactory;
    @Autowired
    private AdminServiceAddressLocator adminServiceAddressLocator;
  
  
    @PostConstruct
    private void postConstruct() {
      restTemplate = restTemplateFactory.getObject();
    }
  
    public <T> T get(Env env, String path, Class<T> responseType, Object... urlVariables)
        throws RestClientException {
      return execute(HttpMethod.GET, env, path, null, responseType, urlVariables);
    }
  
    public <T> ResponseEntity<T> get(Env env, String path, ParameterizedTypeReference<T> reference,
                                     Object... uriVariables)
        throws RestClientException {
  
      return exchangeGet(env, path, reference, uriVariables);
    }
  
    public <T> T post(Env env, String path, Object request, Class<T> responseType, Object... uriVariables)
        throws RestClientException {
      return execute(HttpMethod.POST, env, path, request, responseType, uriVariables);
    }
  
    public void put(Env env, String path, Object request, Object... urlVariables) throws RestClientException {
      execute(HttpMethod.PUT, env, path, request, null, urlVariables);
    }
  
    public void delete(Env env, String path, Object... urlVariables) throws RestClientException {
      execute(HttpMethod.DELETE, env, path, null, null, urlVariables);
    }
  
    private <T> T execute(HttpMethod method, Env env, String path, Object request, Class<T> responseType,
                          Object... uriVariables) {
  
      if (path.startsWith("/")) {
        path = path.substring(1, path.length());
      }
  
      String uri = uriTemplateHandler.expand(path, uriVariables).getPath();
      Transaction ct = Tracer.newTransaction("AdminAPI", uri);
      ct.addData("Env", env);
  
      List<ServiceDTO> services = getAdminServices(env, ct);
  
      for (ServiceDTO serviceDTO : services) {
        try {
  
          T result = doExecute(method, serviceDTO, path, request, responseType, uriVariables);
  
          ct.setStatus(Transaction.SUCCESS);
          ct.complete();
          return result;
        } catch (Throwable t) {
          logger.error("Http request failed, uri: {}, method: {}", uri, method, t);
          Tracer.logError(t);
          if (canRetry(t, method)) {
            Tracer.logEvent(TracerEventType.API_RETRY, uri);
          } else {//biz exception rethrow
            ct.setStatus(t);
            ct.complete();
            throw t;
          }
        }
      }
  
      //all admin server down
      ServiceException e =
          new ServiceException(String.format("Admin servers are unresponsive. meta server address: %s, admin servers: %s",
                                             MetaDomainConsts.getDomain(env), services));
      ct.setStatus(e);
      ct.complete();
      throw e;
    }
  
    private <T> ResponseEntity<T> exchangeGet(Env env, String path, ParameterizedTypeReference<T> reference,
                                              Object... uriVariables) {
      if (path.startsWith("/")) {
        path = path.substring(1, path.length());
      }
  
      String uri = uriTemplateHandler.expand(path, uriVariables).getPath();
      Transaction ct = Tracer.newTransaction("AdminAPI", uri);
      ct.addData("Env", env);
  
      List<ServiceDTO> services = getAdminServices(env, ct);
  
      for (ServiceDTO serviceDTO : services) {
        try {
  
          ResponseEntity<T> result =
              restTemplate.exchange(parseHost(serviceDTO) + path, HttpMethod.GET, null, reference, uriVariables);
  
          ct.setStatus(Transaction.SUCCESS);
          ct.complete();
          return result;
        } catch (Throwable t) {
          logger.error("Http request failed, uri: {}, method: {}", uri, HttpMethod.GET, t);
          Tracer.logError(t);
          if (canRetry(t, HttpMethod.GET)) {
            Tracer.logEvent(TracerEventType.API_RETRY, uri);
          } else {// biz exception rethrow
            ct.setStatus(t);
            ct.complete();
            throw t;
          }
  
        }
      }
  
      //all admin server down
      ServiceException e =
          new ServiceException(String.format("Admin servers are unresponsive. meta server address: %s, admin servers: %s",
                                             MetaDomainConsts.getDomain(env), services));
      ct.setStatus(e);
      ct.complete();
      throw e;
  
    }
  
    private List<ServiceDTO> getAdminServices(Env env, Transaction ct) {
  
      List<ServiceDTO> services = adminServiceAddressLocator.getServiceList(env);
  
      if (CollectionUtils.isEmpty(services)) {
        ServiceException e = new ServiceException(String.format("No available admin server."
                                                                + " Maybe because of meta server down or all admin server down. "
                                                                + "Meta server address: %s",
                                                                MetaDomainConsts.getDomain(env)));
        ct.setStatus(e);
        ct.complete();
        throw e;
      }
  
      return services;
    }
  
    private <T> T doExecute(HttpMethod method, ServiceDTO service, String path, Object request,
                            Class<T> responseType,
                            Object... uriVariables) {
      T result = null;
      switch (method) {
        case GET:
          result = restTemplate.getForObject(parseHost(service) + path, responseType, uriVariables);
          break;
        case POST:
          result =
              restTemplate.postForEntity(parseHost(service) + path, request, responseType, uriVariables).getBody();
          break;
        case PUT:
          restTemplate.put(parseHost(service) + path, request, uriVariables);
          break;
        case DELETE:
          restTemplate.delete(parseHost(service) + path, uriVariables);
          break;
        default:
          throw new UnsupportedOperationException(String.format("unsupported http method(method=%s)", method));
      }
      return result;
    }
  
    private String parseHost(ServiceDTO serviceAddress) {
      return serviceAddress.getHomepageUrl() + "/";
    }
  
    //post,delete,put请求在admin server处理超时情况下不重试
    private boolean canRetry(Throwable e, HttpMethod method) {
      Throwable nestedException = e.getCause();
      if (method == HttpMethod.GET) {
        return nestedException instanceof SocketTimeoutException
               || nestedException instanceof HttpHostConnectException
               || nestedException instanceof ConnectTimeoutException;
      } else {
        return nestedException instanceof HttpHostConnectException
               || nestedException instanceof ConnectTimeoutException;
      }
    }
  
  }
  