All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ifengxue.http.proxy.RequestInvoker Maven / Gradle / Ivy

/*
 * Copyright 2019 https://www.ifengxue.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.ifengxue.http.proxy;

import com.ifengxue.http.HttpClientException;
import com.ifengxue.http.annotation.BodyType;
import com.ifengxue.http.annotation.Delete;
import com.ifengxue.http.annotation.Get;
import com.ifengxue.http.annotation.Head;
import com.ifengxue.http.annotation.HttpMethod;
import com.ifengxue.http.annotation.Patch;
import com.ifengxue.http.annotation.Post;
import com.ifengxue.http.annotation.Put;
import com.ifengxue.http.annotation.ResponseType;
import com.ifengxue.http.annotation.Rest;
import com.ifengxue.http.collection.MultiMap;
import com.ifengxue.http.collection.MultiValueMap;
import com.ifengxue.http.contract.Callback;
import com.ifengxue.http.contract.HttpOperations;
import com.ifengxue.http.contract.HttpResponse;
import com.ifengxue.http.executor.HttpExecutor;
import com.ifengxue.http.executor.HttpExecutorFactory;
import com.ifengxue.http.executor.Request;
import com.ifengxue.http.executor.Request.Builder;
import com.ifengxue.http.parser.HttpParser;
import com.ifengxue.http.parser.HttpParserFactory;
import com.ifengxue.http.parser.StreamHttpParser;
import com.ifengxue.http.util.TypeUtil;
import com.ifengxue.http.util.Version;
import io.mikael.urlbuilder.UrlBuilder;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.CloseableHttpClient;

/**
 * 请求调用代理
 */
public class RequestInvoker implements InvocationHandler, HttpClientConfig, HttpOperations {

  private static final Timeout NOT_SET_TIMEOUT = new Timeout();
  private final Class proxyInterface;
  private final HttpExecutorFactory executorFactory = new HttpExecutorFactory();
  private final HttpParserFactory parserFactory = new HttpParserFactory();
  private final LinkedList interceptors = new LinkedList<>();
  private final Map headers = new HashMap<>();
  private final MultiMap parameters = new MultiValueMap<>();
  private final Map httpOperationsDelegateMethod = new ConcurrentHashMap<>();
  private final Rest rest;
  private volatile URL prefixUrl;
  private Charset charset = UTF_8;
  private HttpHost proxy;
  private Timeout timeout = NOT_SET_TIMEOUT;
  private volatile UsernamePasswordCredentials basicAuthCredentials;
  private AtomicBoolean basicAuthInterceptorHasSet = new AtomicBoolean(false);
  /**
   * 自定义的http client
   */
  private CloseableHttpClient customHttpClient;
  private ExecutorService threadPool;

  private RequestInvoker(Class proxyInterface) {
    this.proxyInterface = proxyInterface;
    rest = proxyInterface.getAnnotation(Rest.class);
    setUserAgent("Http-client-proxy/" + Version.getVersion() + " Java/" + SystemUtils.JAVA_VERSION);
  }

  /**
   * 使用Thread.currentThread().getContextClassLoader()作为类加载器创建请求代理
   *
   * @param proxyInterface 被代理的接口
   */
  public static  T create(Class proxyInterface) {
    return create(proxyInterface, Thread.currentThread().getContextClassLoader());
  }

  /**
   * 创建请求代理
   *
   * @param proxyInterface 被代理的接口
   * @param classLoader 类加载器
   */
  @SuppressWarnings("unchecked")
  public static  T create(Class proxyInterface, ClassLoader classLoader) {
    Objects.requireNonNull(proxyInterface, "proxyInterface cannot be null");
    if (!proxyInterface.isInterface()) {
      throw new IllegalArgumentException(proxyInterface.getName() + " not an interface");
    }
    classLoader = classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader;
    // 当Thread.currentThread.getContextClassLoader()为null时重新赋值
    classLoader = classLoader == null ? RequestInvoker.class.getClassLoader() : classLoader;
    return (T) Proxy.newProxyInstance(classLoader,
        new Class[]{proxyInterface, HttpClientConfig.class, HttpOperations.class},
        new RequestInvoker(proxyInterface));
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (method.getName().equals("toString") && args == null) {
      return proxyInterface.getSimpleName() + " is proxy by " + getClass().getSimpleName() + "!";
    }
    if (method.getName().equals("hashCode") && args == null) {
      return Objects.hashCode(this);
    }
    if (method.getName().equals("equals") && args != null && args.length == 1) {
      return Objects.equals(this, args[0]);
    }
    if (method.getDeclaringClass() == HttpClientConfig.class) {
      return method.invoke(this, args);
    }
    if (method.isDefault()) {
      Constructor constructor = Lookup.class.getDeclaredConstructor(Class.class);
      constructor.setAccessible(true);
      return constructor.newInstance(method.getDeclaringClass())
          .in(method.getDeclaringClass())
          .unreflectSpecial(method, method.getDeclaringClass())
          .bindTo(proxy)
          .invokeWithArguments(args);
    }
    doInit();
    // invoke after init config
    if (method.getDeclaringClass() == HttpOperations.class) {
      return findHttpOperationsDelegateMethod(method).invoke(this, createNewArgs(method, args));
    }

    RequestMethodHolder requestMethodHolder = createHolder(method);
    MultiMap parameterMap = Resolver.resolveParameters(method, args, requestMethodHolder.getBodyType());

    // 解析request
    Request request = Request.Builder.newBuilder(requestMethodHolder.getMethod())
        .setPrefixUrl(prefixUrl)
        .setSuffixUrl(requestMethodHolder.getSuffixUrl())
        .addHeaders(headers)
        .addHeaders(Resolver.resolveHeaders(method))
        .addHeaders(Resolver.resolveParamHeaders(method, args))
        .addParameters(parameters)
        .addParameters(parameterMap)
        .setBody(Resolver.resolveBody(method, args))
        .addQueryParameterNames(Resolver.resolveQueryParameterNames(method, args))
        .setCallbackHandler(Resolver.findCallbackHandler(method, args))
        .build();
    if (request.getCallbackHandler() == null) {
      Supplier supplier = new HttpInvokerSupplier(method, request, requestMethodHolder);
      if (TypeUtil.isAsyncType(method.getReturnType())) {
        return CompletableFuture.supplyAsync(supplier, getThreadPool());
      }
      return supplier.get();
    }
    // do callback
    @SuppressWarnings("unchecked")
    Callback callback = (Callback) request.getCallbackHandler().getCallback();
    Supplier supplier = new HttpInvokerSupplier(method, request, requestMethodHolder.getBodyType(),
        requestMethodHolder.getResponseType(), request.getCallbackHandler().getResponseType());
    CompletableFuture.supplyAsync(supplier, getThreadPool())
        .whenCompleteAsync(callback::call, getThreadPool());
    return null;
  }

  private Object[] createNewArgs(Method method, Object[] args) {
    Object[] newArgs = new Object[method.getParameterTypes().length + 1];
    if (newArgs.length > 1) {
      System.arraycopy(args, 0, newArgs, 1, args.length);
    }
    newArgs[0] = method;
    return newArgs;
  }

  private Method findHttpOperationsDelegateMethod(Method method) {
    return httpOperationsDelegateMethod.computeIfAbsent(method, m -> {
      Class[] parameterTypes = method.getParameterTypes();
      Class[] newParameterTypes = new Class[parameterTypes.length + 1];
      if (parameterTypes.length > 0) {
        System.arraycopy(parameterTypes, 0, newParameterTypes, 1, parameterTypes.length);
      }
      newParameterTypes[0] = Method.class;
      try {
        Method delegateMethod = RequestInvoker.class.getDeclaredMethod(method.getName(), newParameterTypes);
        delegateMethod.setAccessible(true);
        return delegateMethod;
      } catch (NoSuchMethodException e) {
        throw new IllegalStateException("Cannot find matched delegate method " + method.getName());
      }
    });
  }

  /**
   * 创建http executor,并设置基本属性
   */
  private HttpExecutor createHttpExecutor(BodyType bodyType) {
    HttpExecutor executor;
    if (customHttpClient == null) {
      executor = executorFactory.getHttpExecutor(bodyType);
    } else {
      executor = executorFactory.getHttpExecutor(bodyType, customHttpClient);
    }
    executor.setCharset(charset);
    executor.setProxy(this.proxy);
    executor.setTimeout(timeout.socketTimeout, timeout.connectTimeout);
    return executor;
  }

  private void closeHttpResponse(HttpResponse httpResponse) {
    if (httpResponse == null) {
      return;
    }
    try {
      httpResponse.close();
    } catch (Exception ex) {
      // ignore
    }
  }

  private RequestMethodHolder createHolder(Method method) {
    if (method.isAnnotationPresent(Get.class)) {
      Get get = method.getAnnotation(Get.class);
      return new RequestMethodHolder(HttpMethod.GET.name(), get.value(), get.bodyType(), get.responseType());
    } else if (method.isAnnotationPresent(Post.class)) {
      Post post = method.getAnnotation(Post.class);
      return new RequestMethodHolder(HttpMethod.POST.name(), post.value(), post.bodyType(), post.responseType());
    } else if (method.isAnnotationPresent(Put.class)) {
      Put put = method.getAnnotation(Put.class);
      return new RequestMethodHolder(HttpMethod.PUT.name(), put.value(), put.bodyType(), put.responseType());
    } else if (method.isAnnotationPresent(Patch.class)) {
      Patch patch = method.getAnnotation(Patch.class);
      return new RequestMethodHolder(HttpMethod.PATCH.name(), patch.value(), patch.bodyType(), patch.responseType());
    } else if (method.isAnnotationPresent(Delete.class)) {
      Delete delete = method.getAnnotation(Delete.class);
      return new RequestMethodHolder(HttpMethod.DELETE.name(), delete.value(), delete.bodyType(),
          delete.responseType());
    } else if (method.isAnnotationPresent(Head.class)) {
      Head head = method.getAnnotation(Head.class);
      return new RequestMethodHolder(HttpMethod.DELETE.name(), head.value(), head.bodyType(),
          ResponseType.HEADER);
    } else {
      throw new IllegalStateException("method does not specify a matching annotation " + Arrays
          .toString(new String[]{
              Get.class.getName(), Post.class.getName(), Put.class.getName(), Patch.class.getName(),
              Delete.class.getName(), Head.class.getName(),
          }));
    }
  }

  private void initPrefixUrl(String host) {
    if (StringUtils.isBlank(host) && (rest == null || StringUtils.isBlank(rest.value()))) {
      return;
    }
    if (StringUtils.isBlank(host)) {
      prefixUrl = decodeURL(UrlBuilder.fromString(rest.value()).toUrl());
      return;
    }
    if (rest == null || rest.value().isEmpty()) {
      prefixUrl = decodeURL(UrlBuilder.fromString(host).toUrl());
      return;
    }
    UrlBuilder rawHost = UrlBuilder.fromString(rest.value());
    URL replaceHost = decodeURL(UrlBuilder.fromString(host).toUrl());
    prefixUrl = decodeURL(rawHost.withScheme(replaceHost.getProtocol())
        .withHost(replaceHost.getHost())
        .withPort(replaceHost.getPort() == -1 ? null : replaceHost.getPort())
        .withUserInfo(replaceHost.getUserInfo())
        .withPath(replaceHost.getPath())
        .withQuery(replaceHost.getQuery())
        .toUrl());
  }

  private URL decodeURL(URL url) {
    try {
      return new URL(URLDecoder.decode(url.toString(), StandardCharsets.UTF_8.name()));
    } catch (MalformedURLException e) {
      throw new IllegalArgumentException("invalid url:" + url.toString(), e);
    } catch (UnsupportedEncodingException e) {
      throw new IllegalArgumentException("invalid charset", e);
    }
  }

  @Override
  public synchronized void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  @Override
  public synchronized void removeInterceptors(Interceptor interceptor) throws NoSuchElementException {
    if (!interceptors.removeIf(other -> other == interceptor)) {
      throw new NoSuchElementException("No matching interceptors, interceptor instance is " + interceptor);
    }
  }

  @Override
  public synchronized void removeInterceptors(Class interceptorClass)
      throws NoSuchElementException {
    if (!interceptors.removeIf(interceptor -> interceptor.getClass() == interceptorClass)) {
      throw new NoSuchElementException("No matching interceptors, interceptor type is " + interceptorClass.getName());
    }
  }

  @Override
  public void setCharset(Charset charset) {
    this.charset = charset;
  }

  @Override
  public synchronized void setUserAgent(String userAgent) {
    addHeader(HttpHeaders.USER_AGENT, userAgent);
  }

  @Override
  public synchronized void addHeader(String name, String value) {
    headers.put(name, value);
  }

  @Override
  public synchronized void addParameter(String name, Object value) {
    parameters.put(name, value);
  }

  @Override
  public void setHost(String host) {
    if (StringUtils.isBlank(host)) {
      throw new IllegalArgumentException("host can't be empty value");
    }
    synchronized (this) {
      this.prefixUrl = null;
      initPrefixUrl(host);
    }
  }

  @Override
  public synchronized void setProxy(String host, int port, String scheme) {
    proxy = new HttpHost(host, port, scheme);
  }

  @Override
  public synchronized void setTimeout(int socketTimeout, int connectTimeout) {
    timeout = new Timeout(socketTimeout, connectTimeout);
  }

  @Override
  public synchronized void setHttpClient(CloseableHttpClient httpClient) {
    this.customHttpClient = Objects.requireNonNull(httpClient);
  }

  private ExecutorService getThreadPool() {
    return Optional.ofNullable(threadPool).orElseGet(HttpExecutorFactory::getDefaultThreadPool);
  }

  @Override
  public synchronized void setThreadPool(ExecutorService threadPool) {
    this.threadPool = threadPool;
  }

  @Override
  public synchronized void setBasicAuth(String username, String password) {
    if (basicAuthInterceptorHasSet.get()) {
      removeInterceptors(BasicAuthInterceptor.class);
    }
    basicAuthCredentials = new UsernamePasswordCredentials(username, password);
    basicAuthInterceptorHasSet.set(false);
  }

  private void doInit() {
    if (prefixUrl == null) {
      synchronized (this) {
        if (prefixUrl == null) {
          initPrefixUrl(null);
        }
      }
    }
    // 设置 http basic auth 拦截器
    if (basicAuthCredentials != null && !basicAuthInterceptorHasSet.get()) {
      synchronized (this) {
        if (!basicAuthInterceptorHasSet.get()) {
          interceptors.addFirst(new BasicAuthInterceptor(
              basicAuthCredentials.getUserName(), basicAuthCredentials.getPassword(), charset));
          basicAuthInterceptorHasSet.set(true);
        }
      }
    }
  }

  @Override
  public  T exchange(@Nonnull String url, @Nonnull HttpMethod method, @Nonnull BodyType bodyType,
      @Nonnull ResponseType responseType, @Nonnull Type responseEntityType, @Nullable Map httpHeaders,
      @Nullable Map uriVariables, @Nullable Object requestBody) {
    throw new UnsupportedOperationException("Delegate to private exchange method");
  }

  @SuppressWarnings({"unchecked", "unused"})
  private  T exchange(@Nonnull Method invokeMethod, @Nonnull String url, @Nonnull HttpMethod method,
      @Nonnull BodyType bodyType, @Nonnull ResponseType responseType, @Nonnull Type responseEntityType,
      @Nullable Map httpHeaders, @Nullable Map uriVariables,
      @Nullable Object requestBody) {
    doInit();
    MultiMap uriVariableMultiMap = Resolver.resolveParameters(uriVariables);
    Request request = Builder.newBuilder(method.name())
        .setPrefixUrl(prefixUrl)
        .setSuffixUrl(url)
        .addHeaders(headers)
        .addHeaders(Optional.ofNullable(httpHeaders).orElseGet(Collections::emptyMap))
        .addParameters(parameters)
        .addParameters(uriVariableMultiMap)
        .setBody(requestBody)
        .addQueryParameterNames(uriVariableMultiMap.keySet())
        .build();
    Supplier supplier = new HttpInvokerSupplier(invokeMethod, request, bodyType, responseType,
        responseEntityType);
    if (!TypeUtil.isAsyncType(responseEntityType)) {
      return (T) supplier.get();
    }
    return (T) CompletableFuture.supplyAsync(supplier, getThreadPool());
  }

  @AllArgsConstructor
  @Getter
  @ToString
  static class Timeout {

    private final int socketTimeout;
    private final int connectTimeout;

    public Timeout() {
      this(-1, -1);
    }
  }

  @AllArgsConstructor
  @Getter
  private static class RequestMethodHolder {

    private final String method;
    private final String suffixUrl;
    private final BodyType bodyType;
    private final ResponseType responseType;
  }

  private class HttpInvokerSupplier implements Supplier {

    private final Method method;
    private final Request request;
    private final BodyType bodyType;
    private final ResponseType responseType;
    private final Type responseEntityType;

    HttpInvokerSupplier(Method method, Request request, RequestMethodHolder requestMethodHolder) {
      this(method, request, requestMethodHolder.getBodyType(), requestMethodHolder.getResponseType(),
          method.getGenericReturnType());
    }

    HttpInvokerSupplier(Method method, Request request, BodyType bodyType, ResponseType responseType,
        Type responseEntityType) {
      this.method = method;
      this.request = request;
      this.bodyType = bodyType;
      this.responseType = responseType;
      this.responseEntityType = responseEntityType;
    }

    @Override
    public Object get() {
      HttpExecutor executor = createHttpExecutor(bodyType);
      try {
        // 执行前置拦截
        for (Interceptor interceptor : interceptors) {
          Object result = interceptor.beforeRequest(method, request, bodyType, responseType, executor);
          if (result != null) {
            return result;
          }
        }
      } catch (Exception e) {
        if (e instanceof HttpClientException) {
          throw e;
        }
        throw new HttpClientException("error executing pre interceptor", e);
      }
      // 执行HTTP请求
      HttpResponse httpResponse;
      try {
        httpResponse = executor
            .execute(request, bodyType, responseType);
      } catch (IOException e) {
        throw new HttpClientException("unexpected error on request url " + request.getUrl(), e);
      }
      HttpParser httpParser = parserFactory.getHttpParser(responseType);
      try {
        // 执行后置拦截
        for (Interceptor interceptor : interceptors) {
          Object result = interceptor
              .beforeParse(method, request, bodyType, responseType, httpResponse, httpParser);
          if (result != null) {
            closeHttpResponse(httpResponse);
            return result;
          }
        }
      } catch (Exception e) {
        closeHttpResponse(httpResponse);
        if (e instanceof HttpClientException) {
          throw (HttpClientException) e;
        }
        throw new HttpClientException("error executing post interceptor", e);
      }
      Object result;
      try {
        result = httpParser.parse(httpResponse, charset, responseEntityType);
      } catch (Exception e) {
        closeHttpResponse(httpResponse);
        if (e instanceof HttpClientException) {
          throw (HttpClientException) e;
        }
        throw new HttpClientException("error parsing http response", e);
      }
      if (httpParser instanceof StreamHttpParser) {
        return result;
      } else {
        closeHttpResponse(httpResponse);
        return result;
      }
    }
  }
}