+ * Note that if you'd like curly braces literally in the body, urlencode them first.
+ *
+ * @see HttpRequest#expand(String, Map)
+ */
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface HttpBody {
+
+ String value();
+}
diff --git a/src/main/java/com/cowave/commons/client/http/annotation/HttpClient.java b/src/main/java/com/cowave/commons/client/http/annotation/HttpClient.java
new file mode 100644
index 0000000..101d75d
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/annotation/HttpClient.java
@@ -0,0 +1,66 @@
+package com.cowave.commons.client.http.annotation;
+
+import com.cowave.commons.client.http.request.ssl.NoopHostnameVerifier;
+import com.cowave.commons.client.http.request.ssl.NoopTlsSocketFactory;
+import org.slf4j.event.Level;
+import org.springframework.core.annotation.AliasFor;
+import com.cowave.commons.client.http.invoke.codec.decoder.JacksonDecoder;
+import com.cowave.commons.client.http.invoke.codec.encoder.JacksonEncoder;
+import org.springframework.stereotype.Component;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+import java.lang.annotation.*;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Target({TYPE})
+@Retention(RUNTIME)
+@Documented
+@Component
+public @interface HttpClient {
+
+ @AliasFor(attribute = "url")
+ String value() default "";
+
+ @AliasFor(attribute = "value")
+ String url() default "";
+
+ String name() default "";
+
+ int poolConnections() default 10;
+
+ int connectTimeout() default 10000;
+
+ String connectTimeoutStr() default "";
+
+ int readTimeout() default 60000;
+
+ String readTimeoutStr() default "";
+
+ int retryTimes() default 0;
+
+ String retryTimesStr() default "";
+
+ int retryInterval() default 1000;
+
+ String retryIntervalStr() default "";
+
+ Class> encoder() default JacksonEncoder.class;
+
+ Class> decoder() default JacksonDecoder.class;
+
+ Class extends SSLSocketFactory> sslSocketFactory() default NoopTlsSocketFactory.class;
+
+ Class extends HostnameVerifier> hostnameVerifier() default NoopHostnameVerifier.class;
+
+ Level level() default Level.INFO;
+
+ boolean ignoreError() default false;
+}
diff --git a/src/main/java/com/cowave/commons/client/http/annotation/HttpHeaderMap.java b/src/main/java/com/cowave/commons/client/http/annotation/HttpHeaderMap.java
new file mode 100644
index 0000000..665c5ab
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/annotation/HttpHeaderMap.java
@@ -0,0 +1,62 @@
+package com.cowave.commons.client.http.annotation;
+
+import com.cowave.commons.client.http.HttpInterceptor;
+import com.cowave.commons.client.http.request.HttpRequest;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Map;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A template parameter that can be applied to a Map that contains header
+ * entries, where the keys are Strings that are the header field names and the
+ * values are the header field values. The headers specified by the map will be
+ * applied to the request after all other processing, and will take precedence
+ * over any previously specified header parameters.
+ *
+ * This parameter is useful in cases where different header fields and values
+ * need to be set on an API method on a per-request basis in a thread-safe manner
+ * and independently of client construction. A concrete example of a case
+ * like this are custom metadata header fields (e.g. as "x-amz-meta-*" or
+ * "x-goog-meta-*") where the header field names are dynamic and the range of keys
+ * cannot be determined a priori. The {@link HttpHeaders} annotation does not allow this
+ * because the header fields that it defines are static (it is not possible to add or
+ * remove fields on a per-request basis), and doing this using a custom {@link Target}
+ * or {@link HttpInterceptor} can be cumbersome (it requires more code for per-method
+ * customization, it is difficult to implement in a thread-safe manner and it requires
+ * customization when the client for the API is built).
+ *
+ *
+ * The annotated parameter must be an instance of {@link Map}, and the keys must
+ * be Strings. The header field value of a key will be the value of its toString
+ * method, except in the following cases:
+ *
+ *
+ *
+ *
if the value is null, the value will remain null (rather than converting
+ * to the String "null")
+ *
if the value is an {@link Iterable}, it is converted to a {@link List} of
+ * String objects where each value in the list is either null if the original
+ * value was null or the value's toString representation otherwise.
+ *
+ *
+ * Once this conversion is applied, the query keys and resulting String values
+ * follow the same contract as if they were set using
+ * {@link HttpRequest#header(String, String...)}.
+ */
+@Retention(RUNTIME)
+@Target(PARAMETER)
+@Documented
+public @interface HttpHeaderMap {
+
+}
diff --git a/src/main/java/com/cowave/commons/client/http/annotation/HttpHeaders.java b/src/main/java/com/cowave/commons/client/http/annotation/HttpHeaders.java
new file mode 100644
index 0000000..347b541
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/annotation/HttpHeaders.java
@@ -0,0 +1,55 @@
+package com.cowave.commons.client.http.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Expands headers supplied in the {@code value}. Variables to the the right of the colon are expanded.
+ *
+ * The annotated parameter must be an instance of {@link Map}, and the keys must
+ * be Strings. The query value of a key will be the value of its toString
+ * method, except in the following cases:
+ *
+ *
+ *
+ *
if the value is null, the value will remain null (rather than converting
+ * to the String "null")
+ *
if the value is an {@link Iterable}, it is converted to a {@link List} of
+ * String objects where each value in the list is either null if the original
+ * value was null or the value's toString representation otherwise.
+ *
+ *
+ * Once this conversion is applied, the query keys and resulting String values
+ * follow the same contract as if they were set using
+ * {@link HttpRequest#query(String, String...)}.
+ */
+@Retention(RUNTIME)
+@Target(PARAMETER)
+@Documented
+public @interface HttpParamMap {
+
+ boolean encoded() default false;
+}
diff --git a/src/main/java/com/cowave/commons/client/http/asserts/Asserts.java b/src/main/java/com/cowave/commons/client/http/asserts/Asserts.java
new file mode 100644
index 0000000..b442df7
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/asserts/Asserts.java
@@ -0,0 +1,187 @@
+package com.cowave.commons.client.http.asserts;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class Asserts {
+
+ public static void isTrue(boolean expression, String message, Object... args) {
+ if (!expression) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void isTrue(boolean expression, Supplier errorSupplier) {
+ if (!expression) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void isFalse(boolean expression, String message, Object... args) {
+ if (expression) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void isFalse(boolean expression, Supplier errorSupplier) {
+ if (expression) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void notEquals(Object a, Object b, String message, Object... args) {
+ if (Objects.equals(a, b)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void notEquals(Object a, Object b, Supplier errorSupplier) {
+ if (Objects.equals(a, b)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void equals(Object a, Object b, String message, Object... args) {
+ if (!Objects.equals(a, b)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void equals(Object a, Object b, Supplier errorSupplier) {
+ if (!Objects.equals(a, b)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void notBlank(String text, String message, Object... args) {
+ if (StringUtils.isBlank(text)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void notBlank(String text, Supplier errorSupplier) {
+ if (StringUtils.isBlank(text)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void isBlank(String text, String message, Object... args) {
+ if (StringUtils.isNotBlank(text)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void isBlank(String text, Supplier errorSupplier) {
+ if (StringUtils.isNotBlank(text)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void notNull(Object object, String message, Object... args) {
+ if (object == null) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void notNull(Object object, Supplier errorSupplier) {
+ if (object == null) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void isNull(Object object, String message, Object... args) {
+ if (object != null) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void isNull(Object object, Supplier errorSupplier) {
+ if (object != null) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void notEmpty(Map, ?> map, String message, Object... args) {
+ if (ObjectUtils.isEmpty(map)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void notEmpty(Map, ?> map, Supplier errorSupplier) {
+ if (ObjectUtils.isEmpty(map)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void isEmpty(Map, ?> map, String message, Object... args) {
+ if (ObjectUtils.isNotEmpty(map)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void isEmpty(Map, ?> map, Supplier errorSupplier) {
+ if (ObjectUtils.isNotEmpty(map)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void notEmpty(Collection> collection, String message, Object... args) {
+ if (CollectionUtils.isEmpty(collection)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void notEmpty(Collection> collection, Supplier errorSupplier) {
+ if (CollectionUtils.isEmpty(collection)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void isEmpty(Collection> collection, String message, Object... args) {
+ if (!CollectionUtils.isEmpty(collection)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void isEmpty(Collection> collection, Supplier errorSupplier) {
+ if (!CollectionUtils.isEmpty(collection)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+ public static void notEmpty(Object[] array, String message, Object... args) {
+ if (ObjectUtils.isEmpty(array)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void notEmpty(Object[] array, Supplier errorSupplier) {
+ if (ObjectUtils.isEmpty(array)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+
+
+ public static void isEmpty(Object[] array, String message, Object... args) {
+ if (!ObjectUtils.isEmpty(array)) {
+ throw new AssertsException(message, args);
+ }
+ }
+
+ public static void isEmpty(Object[] array, Supplier errorSupplier) {
+ if (!ObjectUtils.isEmpty(array)) {
+ throw new AssertsException(errorSupplier.get());
+ }
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/asserts/AssertsException.java b/src/main/java/com/cowave/commons/client/http/asserts/AssertsException.java
new file mode 100644
index 0000000..bc22c10
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/asserts/AssertsException.java
@@ -0,0 +1,17 @@
+package com.cowave.commons.client.http.asserts;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class AssertsException extends RuntimeException {
+
+ public AssertsException(String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args));
+ }
+
+ public AssertsException(Throwable cause, String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args), cause);
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/asserts/HttpAsserts.java b/src/main/java/com/cowave/commons/client/http/asserts/HttpAsserts.java
new file mode 100644
index 0000000..9ad5ee4
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/asserts/HttpAsserts.java
@@ -0,0 +1,271 @@
+package com.cowave.commons.client.http.asserts;
+
+import com.cowave.commons.client.http.response.ResponseCode;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class HttpAsserts {
+
+ public static void isTrue(boolean expression, ResponseCode responseCode){
+ if (!expression) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void isTrue(boolean expression, int status, String code, String message, Object... args) {
+ if (!expression) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void isTrue(boolean expression, int status, String code, Supplier errorSupplier) {
+ if (!expression) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void isFalse(boolean expression, ResponseCode responseCode){
+ if (expression) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void isFalse(boolean expression, int status, String code, String message, Object... args) {
+ if (expression) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void isFalse(boolean expression, int status, String code, Supplier errorSupplier) {
+ if (expression) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void notEquals(Object a, Object b, ResponseCode responseCode){
+ if (Objects.equals(a, b)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void notEquals(Object a, Object b, int status, String code, String message, Object... args) {
+ if (Objects.equals(a, b)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void notEquals(Object a, Object b, int status, String code, Supplier errorSupplier) {
+ if (Objects.equals(a, b)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void equals(Object a, Object b, ResponseCode responseCode){
+ if (!Objects.equals(a, b)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void equals(Object a, Object b, int status, String code, String message, Object... args) {
+ if (!Objects.equals(a, b)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void equals(Object a, Object b, int status, String code, Supplier errorSupplier) {
+ if (!Objects.equals(a, b)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void notBlank(String text, ResponseCode responseCode){
+ if (StringUtils.isBlank(text)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void notBlank(String text, int status, String code, String message, Object... args) {
+ if (StringUtils.isBlank(text)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void notBlank(String text, int status, String code, Supplier errorSupplier) {
+ if (StringUtils.isBlank(text)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void isBlank(String text, ResponseCode responseCode){
+ if (StringUtils.isNotBlank(text)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void isBlank(String text, int status, String code, String message, Object... args) {
+ if (StringUtils.isNotBlank(text)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void isBlank(String text, int status, String code, Supplier errorSupplier) {
+ if (StringUtils.isNotBlank(text)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void notNull(Object object, ResponseCode responseCode){
+ if (object == null) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void notNull(Object object, int status, String code, String message, Object... args) {
+ if (object == null) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void notNull(Object object, int status, String code, Supplier errorSupplier) {
+ if (object == null) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void isNull(Object object, ResponseCode responseCode){
+ if (object != null) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void isNull(Object object, int status, String code, String message, Object... args) {
+ if (object != null) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void isNull(Object object, int status, String code, Supplier errorSupplier) {
+ if (object != null) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void notEmpty(Map, ?> map, ResponseCode responseCode){
+ if (ObjectUtils.isEmpty(map)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void notEmpty(Map, ?> map, int status, String code, String message, Object... args) {
+ if (ObjectUtils.isEmpty(map)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void notEmpty(Map, ?> map, int status, String code, Supplier errorSupplier) {
+ if (ObjectUtils.isEmpty(map)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void isEmpty(Map, ?> map, ResponseCode responseCode){
+ if (ObjectUtils.isNotEmpty(map)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void isEmpty(Map, ?> map, int status, String code, String message, Object... args) {
+ if (ObjectUtils.isNotEmpty(map)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void isEmpty(Map, ?> map, int status, String code, Supplier errorSupplier) {
+ if (ObjectUtils.isNotEmpty(map)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void notEmpty(Collection> collection, ResponseCode responseCode){
+ if (CollectionUtils.isEmpty(collection)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void notEmpty(Collection> collection, int status, String code, String message, Object... args) {
+ if (CollectionUtils.isEmpty(collection)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void notEmpty(Collection> collection, int status, String code, Supplier errorSupplier) {
+ if (CollectionUtils.isEmpty(collection)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void isEmpty(Collection> collection, ResponseCode responseCode){
+ if (!CollectionUtils.isEmpty(collection)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void isEmpty(Collection> collection, int status, String code, String message, Object... args) {
+ if (!CollectionUtils.isEmpty(collection)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void isEmpty(Collection> collection, int status, String code, Supplier errorSupplier) {
+ if (!CollectionUtils.isEmpty(collection)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void notEmpty(Object[] array, ResponseCode responseCode){
+ if (ObjectUtils.isEmpty(array)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void notEmpty(Object[] array, int status, String code, String message, Object... args) {
+ if (ObjectUtils.isEmpty(array)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void notEmpty(Object[] array, int status, String code, Supplier errorSupplier) {
+ if (ObjectUtils.isEmpty(array)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+
+ public static void isEmpty(Object[] array, ResponseCode responseCode){
+ if (!ObjectUtils.isEmpty(array)) {
+ throw new HttpException(responseCode);
+ }
+ }
+
+ public static void isEmpty(Object[] array, int status, String code, String message, Object... args) {
+ if (!ObjectUtils.isEmpty(array)) {
+ throw new HttpException(status, code, message, args);
+ }
+ }
+
+ public static void isEmpty(Object[] array, int status, String code, Supplier errorSupplier) {
+ if (!ObjectUtils.isEmpty(array)) {
+ throw new HttpException(status, code, errorSupplier.get());
+ }
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/asserts/HttpException.java b/src/main/java/com/cowave/commons/client/http/asserts/HttpException.java
new file mode 100644
index 0000000..df87fbe
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/asserts/HttpException.java
@@ -0,0 +1,66 @@
+package com.cowave.commons.client.http.asserts;
+
+import com.cowave.commons.client.http.response.ResponseCode;
+import lombok.Getter;
+
+import static com.cowave.commons.client.http.HttpCode.SERVICE_ERROR;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Getter
+public class HttpException extends RuntimeException {
+
+ private final int status;
+
+ private final String code;
+
+ public HttpException(String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args));
+ this.code = SERVICE_ERROR.getCode();
+ this.status = SERVICE_ERROR.getStatus();
+ }
+
+ public HttpException(Throwable cause, String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args), cause);
+ this.code = SERVICE_ERROR.getCode();
+ this.status = SERVICE_ERROR.getStatus();
+ }
+
+ public HttpException(ResponseCode responseCode) {
+ super(I18Messages.translateIfNeed(responseCode.getMsg()));
+ this.code = responseCode.getCode();
+ this.status = responseCode.getStatus();
+ }
+
+ public HttpException(ResponseCode responseCode, String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args));
+ this.code = responseCode.getCode();
+ this.status = responseCode.getStatus();
+ }
+
+ public HttpException(ResponseCode responseCode, Throwable cause, String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args), cause);
+ this.code = responseCode.getCode();
+ this.status = responseCode.getStatus();
+ }
+
+ public HttpException(int status, String code) {
+ this.code = code;
+ this.status = status;
+ }
+
+ public HttpException(int status, String code, String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args));
+ this.code = code;
+ this.status = status;
+ }
+
+ public HttpException(int status, String code, Throwable cause, String message, Object... args) {
+ super(I18Messages.translateIfNeed(message, args), cause);
+ this.code = code;
+ this.status = status;
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/asserts/HttpHintException.java b/src/main/java/com/cowave/commons/client/http/asserts/HttpHintException.java
new file mode 100644
index 0000000..35e05e4
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/asserts/HttpHintException.java
@@ -0,0 +1,45 @@
+package com.cowave.commons.client.http.asserts;
+
+import com.cowave.commons.client.http.response.ResponseCode;
+import lombok.Getter;
+
+/**
+ * 不打印异常日志
+ *
+ * @author shanhuiming
+ */
+@Getter
+public class HttpHintException extends HttpException {
+
+ public HttpHintException(String message, Object... args) {
+ super(message, args);
+ }
+
+ public HttpHintException(Throwable cause, String message, Object... args) {
+ super(cause, message, args);
+ }
+
+ public HttpHintException(ResponseCode responseCode) {
+ super(responseCode);
+ }
+
+ public HttpHintException(ResponseCode responseCode, String message, Object... args) {
+ super(responseCode, message, args);
+ }
+
+ public HttpHintException(ResponseCode responseCode, Throwable cause, String message, Object... args) {
+ super(responseCode, cause, message, args);
+ }
+
+ public HttpHintException(int status, String code) {
+ super(status, code);
+ }
+
+ public HttpHintException(int status, String code, String message, Object... args) {
+ super(status, code, message, args);
+ }
+
+ public HttpHintException(int status, String code, Throwable cause, String message, Object... args) {
+ super(status, code, cause, message, args);
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/asserts/HttpWarnException.java b/src/main/java/com/cowave/commons/client/http/asserts/HttpWarnException.java
new file mode 100644
index 0000000..419b5b5
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/asserts/HttpWarnException.java
@@ -0,0 +1,45 @@
+package com.cowave.commons.client.http.asserts;
+
+import com.cowave.commons.client.http.response.ResponseCode;
+import lombok.Getter;
+
+/**
+ * 异常日志只打印e.message
+ *
+ * @author shanhuiming
+ */
+@Getter
+public class HttpWarnException extends HttpException {
+
+ public HttpWarnException(String message, Object... args) {
+ super(message, args);
+ }
+
+ public HttpWarnException(Throwable cause, String message, Object... args) {
+ super(cause, message, args);
+ }
+
+ public HttpWarnException(ResponseCode responseCode) {
+ super(responseCode);
+ }
+
+ public HttpWarnException(ResponseCode responseCode, String message, Object... args) {
+ super(responseCode, message, args);
+ }
+
+ public HttpWarnException(ResponseCode responseCode, Throwable cause, String message, Object... args) {
+ super(responseCode, cause, message, args);
+ }
+
+ public HttpWarnException(int status, String code) {
+ super(status, code);
+ }
+
+ public HttpWarnException(int status, String code, String message, Object... args) {
+ super(status, code, message, args);
+ }
+
+ public HttpWarnException(int status, String code, Throwable cause, String message, Object... args) {
+ super(status, code, cause, message, args);
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/asserts/I18Messages.java b/src/main/java/com/cowave/commons/client/http/asserts/I18Messages.java
new file mode 100644
index 0000000..bafd294
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/asserts/I18Messages.java
@@ -0,0 +1,79 @@
+package com.cowave.commons.client.http.asserts;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.text.StringSubstitutor;
+import org.springframework.context.MessageSource;
+import org.springframework.util.StringUtils;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class I18Messages {
+
+ private static final ThreadLocal LANGUAGE = new TransmittableThreadLocal<>();
+
+ private static MessageSource messageSource;
+
+ public static void setMessageSource(MessageSource messageSource) {
+ I18Messages.messageSource = messageSource;
+ }
+
+ public static void setLanguage(String language){
+ if(StringUtils.hasText(language)){
+ if(language.toLowerCase().contains("en")) {
+ LANGUAGE.set(new Locale("en", "US"));
+ }else if(language.toLowerCase().contains("zh")) {
+ LANGUAGE.set(new Locale("zh", "CN"));
+ }
+ }
+ }
+
+ public static Locale getLanguage() {
+ Locale local = LANGUAGE.get();
+ if(local != null){
+ return local;
+ }
+ return Locale.CHINA;
+ }
+
+ public static void clearLanguage() {
+ LANGUAGE.remove();
+ }
+
+ public static String msg(String message, Object... args) {
+ return messageSource.getMessage(message, args, message, getLanguage());
+ }
+
+ public static String msgWithDefault(String message, String defaultMessage, Object... args) {
+ return messageSource.getMessage(message, args, defaultMessage, getLanguage());
+ }
+
+ public static String translateIfNeed(String message, Object... args){
+ if(!StringUtils.hasText(message)){
+ return "";
+ }
+ if (message.startsWith("{") && message.endsWith("}")) {
+ message = message.substring(1, message.length() - 1);
+ return messageSource.getMessage(message, args, message, getLanguage());
+ }
+ return message;
+ }
+
+ public static String translateAndReplace(String message, Map args, String prefix, String suffix) {
+ if(!StringUtils.hasText(message)){
+ return "";
+ }
+ String msg = messageSource.getMessage(message, null, message, getLanguage());
+ if(MapUtils.isNotEmpty(args)){
+ StringSubstitutor substitutor = new StringSubstitutor(args, prefix, suffix);
+ return substitutor.replace(msg);
+ }
+ return msg;
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/codec/HttpDecoder.java b/src/main/java/com/cowave/commons/client/http/invoke/codec/HttpDecoder.java
new file mode 100644
index 0000000..a2ad68f
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/codec/HttpDecoder.java
@@ -0,0 +1,16 @@
+package com.cowave.commons.client.http.invoke.codec;
+
+import com.cowave.commons.client.http.response.HttpResponseTemplate;
+import org.slf4j.event.Level;
+
+import java.lang.reflect.Type;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public interface HttpDecoder {
+
+ Object decode(HttpResponseTemplate response, Type type, String url, long cost, int httpCode, Level level) throws Exception;
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/codec/HttpEncoder.java b/src/main/java/com/cowave/commons/client/http/invoke/codec/HttpEncoder.java
new file mode 100644
index 0000000..f1f9dda
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/codec/HttpEncoder.java
@@ -0,0 +1,13 @@
+package com.cowave.commons.client.http.invoke.codec;
+
+import com.cowave.commons.client.http.request.HttpRequest;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public interface HttpEncoder {
+
+ void encode(HttpRequest request, Object object) throws Exception;
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/codec/decoder/JacksonDecoder.java b/src/main/java/com/cowave/commons/client/http/invoke/codec/decoder/JacksonDecoder.java
new file mode 100644
index 0000000..6b3bbf1
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/codec/decoder/JacksonDecoder.java
@@ -0,0 +1,93 @@
+package com.cowave.commons.client.http.invoke.codec.decoder;
+
+import com.cowave.commons.client.http.response.HttpResponseTemplate;
+import com.cowave.commons.client.http.invoke.codec.HttpDecoder;
+import com.cowave.commons.client.http.response.Response;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.event.Level;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.TimeZone;
+
+import static com.cowave.commons.client.http.HttpCode.SUCCESS;
+import static org.slf4j.event.Level.WARN;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Slf4j
+public class JacksonDecoder implements HttpDecoder {
+
+ public static final ObjectMapper MAPPER = new ObjectMapper();
+
+ static {
+ MAPPER.setTimeZone(TimeZone.getDefault());
+ MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
+
+ private final ObjectMapper mapper;
+
+ public JacksonDecoder() {
+ this(MAPPER);
+ }
+
+ public JacksonDecoder(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public Object decode(HttpResponseTemplate response, Type type, String url, long cost, int status, Level level) throws Exception {
+ Reader reader = new InputStreamReader(response.getInputStream(), StandardCharsets.UTF_8);
+ if (!reader.markSupported()) {
+ reader = new BufferedReader(reader, 1);
+ }
+
+ reader.mark(1);
+ if (reader.read() == -1) {
+ return null;
+ }
+ reader.reset();
+
+ if (void.class == type) {
+ if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {}", status, cost, url);
+ }
+ return null;
+ }
+
+ Object obj = mapper.readValue(reader, mapper.constructType(type));
+ if(obj != null){
+ if(Response.class.isAssignableFrom(obj.getClass())){
+ Response> resp = (Response>)obj;
+ if(!Objects.equals(SUCCESS.getCode(), resp.getCode())){
+ log.error(">< {} {}ms {} {code={}, msg={}}", status, cost, url, resp.getCode(), resp.getMsg());
+ }else if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {} {code={}, msg={}}", status, cost, url, resp.getCode(), resp.getMsg());
+ }
+ }else if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {}", status, cost, url);
+ }
+ }else if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {}", status, cost, url);
+ }
+ return obj;
+ }
+
+ public static Object readValue(String json, Type type) throws IOException {
+ return MAPPER.readValue(json, MAPPER.constructType(type));
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/codec/decoder/ResponseDecoder.java b/src/main/java/com/cowave/commons/client/http/invoke/codec/decoder/ResponseDecoder.java
new file mode 100644
index 0000000..034a72d
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/codec/decoder/ResponseDecoder.java
@@ -0,0 +1,83 @@
+package com.cowave.commons.client.http.invoke.codec.decoder;
+
+import com.cowave.commons.client.http.response.HttpResponseTemplate;
+import com.cowave.commons.client.http.response.Response;
+import com.cowave.commons.client.http.asserts.HttpHintException;
+import com.fasterxml.jackson.databind.*;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.event.Level;
+import com.cowave.commons.client.http.invoke.codec.HttpDecoder;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import static com.cowave.commons.client.http.HttpCode.SUCCESS;
+import static org.slf4j.event.Level.WARN;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Slf4j
+public class ResponseDecoder implements HttpDecoder {
+
+ private final ObjectMapper mapper;
+
+ public ResponseDecoder() {
+ this.mapper = JacksonDecoder.MAPPER;
+ }
+
+ public ResponseDecoder(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public Object decode(HttpResponseTemplate response, Type type, String url, long cost, int status, Level level) throws Exception {
+ if (response.getInputStream() == null) {
+ if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {}", status, cost, url);
+ }
+ return null;
+ }
+
+ Reader reader = new InputStreamReader(response.getInputStream(), StandardCharsets.UTF_8);
+ if (!reader.markSupported()) {
+ reader = new BufferedReader(reader, 1);
+ }
+
+ // Read the first byte to see if we have any data
+ reader.mark(1);
+ // Eagerly returning null avoids "No content to map due to end-of-input"
+ if (reader.read() == -1) {
+ if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {}", status, cost, url);
+ }
+ return null;
+ }
+ reader.reset();
+
+ Response> resp = mapper.readValue(reader, Response.class);
+ if(!Objects.equals(SUCCESS.getCode(), resp.getCode())){
+ log.error(">< {} {}ms {} {code={}, msg={}}", status, cost, url, resp.getCode(), resp.getMsg());
+ throw new HttpHintException(status, resp.getCode(), resp.getMsg());
+ }
+
+ if (void.class == type) {
+ if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {} {code={}, msg={}}", status, cost, url, resp.getCode(), resp.getMsg());
+ }
+ return null;
+ }
+
+ if(log.isDebugEnabled() || level.toInt() < WARN.toInt()){
+ log.info(">< {} {}ms {} {code={}, msg={}}", status, cost, url, resp.getCode(), resp.getMsg());
+ }
+ String data = mapper.writeValueAsString(resp.getData());
+ return mapper.readValue(data, mapper.constructType(type));
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/codec/encoder/JacksonEncoder.java b/src/main/java/com/cowave/commons/client/http/invoke/codec/encoder/JacksonEncoder.java
new file mode 100644
index 0000000..0b0eb06
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/codec/encoder/JacksonEncoder.java
@@ -0,0 +1,39 @@
+package com.cowave.commons.client.http.invoke.codec.encoder;
+
+import com.cowave.commons.client.http.invoke.codec.HttpEncoder;
+import com.cowave.commons.client.http.request.HttpRequest;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import java.util.TimeZone;
+
+/**
+ * @author shanhuiming
+ */
+public class JacksonEncoder implements HttpEncoder {
+
+ public static final ObjectMapper MAPPER = new ObjectMapper();
+
+ static {
+ MAPPER.setTimeZone(TimeZone.getDefault());
+ MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ }
+
+ private final ObjectMapper mapper;
+
+ public JacksonEncoder() {
+ this(MAPPER);
+ }
+
+ public JacksonEncoder(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ public void encode(HttpRequest request, Object object) throws JsonProcessingException {
+ request.body(mapper.writeValueAsString(object));
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/exec/HttpClientExecutor.java b/src/main/java/com/cowave/commons/client/http/invoke/exec/HttpClientExecutor.java
new file mode 100644
index 0000000..e39de36
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/exec/HttpClientExecutor.java
@@ -0,0 +1,232 @@
+package com.cowave.commons.client.http.invoke.exec;
+
+import com.cowave.commons.client.http.request.HttpRequestTemplate;
+import com.cowave.commons.client.http.response.HttpResponseTemplate;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NoHttpResponseException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.*;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.mime.content.InputStreamBody;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.GZIPOutputStream;
+
+import static com.cowave.commons.client.http.HttpHeader.*;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class HttpClientExecutor implements HttpExecutor {
+
+ private final HttpClient httpClient;
+
+ public HttpClientExecutor(SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier) {
+ SSLConnectionSocketFactory sslConnectionSocketFactory =
+ new SSLConnectionSocketFactory(sslSocketFactory, hostnameVerifier);
+ Registry registry = RegistryBuilder.create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", sslConnectionSocketFactory).build();
+
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
+ connectionManager.setMaxTotal(100);
+
+ HttpRequestRetryHandler retryHandler = (exception, execCount, context) -> {
+ Integer retryTimes = (Integer) context.getAttribute("retryTimes");
+ Integer retryInterval = (Integer) context.getAttribute("retryInterval");
+ if (retryTimes == null) {
+ retryTimes = 0;
+ }
+ if (execCount > retryTimes) {
+ return false;
+ }
+
+ if (retryInterval == null) {
+ retryInterval = 1000;
+ }
+ try {
+ Thread.sleep(retryInterval);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return false;
+ }
+ return exception instanceof ConnectTimeoutException
+ || exception instanceof NoHttpResponseException
+ || exception instanceof SocketException;
+ };
+
+ HttpClientBuilder httpClientBuilder = HttpClients.custom();
+ httpClientBuilder.setConnectionManager(connectionManager);
+ httpClientBuilder.setRetryHandler(retryHandler);
+ this.httpClient = httpClientBuilder.build();
+ }
+
+ @Override
+ public HttpResponseTemplate execute(HttpRequestTemplate request) throws IOException {
+ HttpRequestBase httpRequest = buildRequest(request);
+ // 超时参数
+ RequestConfig requestConfig = RequestConfig.custom()
+ .setConnectTimeout(request.getConnectTimeout())
+ .setSocketTimeout(request.getReadTimeout()).build();
+ // 重试设置
+ HttpClientContext context = HttpClientContext.create();
+ context.setAttribute("retryTimes", request.getRetryTimes());
+ context.setAttribute("retryInterval", request.getRetryInterval());
+
+ httpRequest.setConfig(requestConfig);
+ HttpResponse httpResponse = httpClient.execute(httpRequest, context);
+
+ int status = httpResponse.getStatusLine().getStatusCode();
+ String reason = httpResponse.getStatusLine().getReasonPhrase();
+
+ // 响应 Header
+ Map> headers = new HashMap<>();
+ for (Header header : httpResponse.getAllHeaders()) {
+ headers.computeIfAbsent(header.getName(), k -> new ArrayList<>()).add(header.getValue());
+ }
+
+ InputStream stream = httpResponse.getEntity() != null ? httpResponse.getEntity().getContent() : null;
+ long length = httpResponse.getEntity() != null ? httpResponse.getEntity().getContentLength() : 0;
+ return new HttpResponseTemplate(status, headers, reason, stream, (int) length);
+ }
+
+ /**
+ * @see GET, DELETE, POST, PATCH, PUT, HEAD, OPTIONS, TRACE, CONNECT
+ */
+ private HttpRequestBase buildRequest(HttpRequestTemplate request) throws IOException {
+ // 请求压缩
+ Collection encodings = request.getHeaders().get(Content_Encoding);
+ // gzip
+ boolean useGzipEncode = encodings != null && encodings.contains("gzip");
+ // deflate
+ boolean useDeflateEncode = encodings != null && encodings.contains("deflate");
+
+ String url = request.getUrl();
+ String method = request.getMethod().toUpperCase();
+ switch (method.toUpperCase()) {
+ case "GET":
+ HttpGet httpGet = new HttpGet(url);
+ setHeader(httpGet, request);
+ return httpGet;
+ case "DELETE":
+ HttpDelete httpDelete = new HttpDelete(url);
+ setHeader(httpDelete, request);
+ return httpDelete;
+ case "POST":
+ HttpPost httpPost = new HttpPost(url);
+ setHeader(httpPost, request);
+ setEntity(httpPost, request, useGzipEncode, useDeflateEncode);
+ return httpPost;
+ case "PATCH":
+ HttpPatch httpPatch = new HttpPatch(url);
+ setHeader(httpPatch, request);
+ setEntity(httpPatch, request, useGzipEncode, useDeflateEncode);
+ return httpPatch;
+ case "PUT":
+ HttpPut httpPut = new HttpPut(url);
+ setHeader(httpPut, request);
+ setEntity(httpPut, request, useGzipEncode, useDeflateEncode);
+ return httpPut;
+ case "HEAD":
+ HttpHead httpHead = new HttpHead(url);
+ setHeader(httpHead, request);
+ return httpHead;
+ case "OPTIONS":
+ HttpOptions httpOptions = new HttpOptions(url);
+ setHeader(httpOptions, request);
+ return httpOptions;
+ case "TRACE":
+ HttpTrace httpTrace = new HttpTrace(url);
+ setHeader(httpTrace, request);
+ return httpTrace;
+ default:
+ throw new UnsupportedOperationException("Unsupported HTTP method: " + method);
+ }
+ }
+
+ private void setEntity(HttpEntityEnclosingRequestBase httpRequest, HttpRequestTemplate request,
+ boolean useGzipEncode, boolean useDeflateEncode) throws IOException {
+ if (request.getBody() != null) {
+ if (useGzipEncode) {
+ try (ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream();
+ GZIPOutputStream gzipOutStream = new GZIPOutputStream(byteArrayOutStream)) {
+ gzipOutStream.write(request.getBody());
+ HttpEntity httpEntity = new ByteArrayEntity(byteArrayOutStream.toByteArray());
+ httpRequest.setEntity(httpEntity);
+ }
+ } else if (useDeflateEncode) {
+ try (ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream();
+ DeflaterOutputStream defeaterOutStream = new DeflaterOutputStream(byteArrayOutStream)) {
+ defeaterOutStream.write(request.getBody());
+ HttpEntity httpEntity = new ByteArrayEntity(byteArrayOutStream.toByteArray());
+ httpRequest.setEntity(httpEntity);
+ }
+ } else {
+ HttpEntity httpEntity = new ByteArrayEntity(request.getBody());
+ httpRequest.setEntity(httpEntity);
+ }
+ } else if (request.getMultiFile() != null || request.getMultiForm() != null) {
+ MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+ builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
+ if (request.getMultiFile() != null) {
+ builder.addPart("file", new InputStreamBody(
+ request.getMultiFile(), ContentType.APPLICATION_OCTET_STREAM, request.getMultiFileName()));
+ }
+ if (request.getMultiForm() != null) {
+ for (Map.Entry entry : request.getMultiForm().entrySet()) {
+ builder.addTextBody(entry.getKey(), entry.getValue().toString());
+ }
+ }
+ HttpEntity httpEntity = builder.build();
+ httpRequest.setEntity(httpEntity);
+ }
+ }
+
+ private void setHeader(HttpRequestBase httpRequest, HttpRequestTemplate request) {
+ boolean hasAcceptHeader = false;
+ for (String field : request.getHeaders().keySet()) {
+ if (field.equalsIgnoreCase(Accept)) {
+ hasAcceptHeader = true;
+ }
+ for (String value : request.getHeaders().get(field)) {
+ if (!field.equals(Content_Length)) {
+ httpRequest.addHeader(field, value);
+ }
+ }
+ }
+
+ // 接受任意类型的响应
+ if (!hasAcceptHeader) {
+ httpRequest.setHeader(Accept, "*/*");
+ }
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/exec/HttpExecutor.java b/src/main/java/com/cowave/commons/client/http/invoke/exec/HttpExecutor.java
new file mode 100644
index 0000000..116dcfd
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/exec/HttpExecutor.java
@@ -0,0 +1,16 @@
+package com.cowave.commons.client.http.invoke.exec;
+
+import com.cowave.commons.client.http.request.HttpRequestTemplate;
+import com.cowave.commons.client.http.response.HttpResponseTemplate;
+
+import java.io.IOException;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public interface HttpExecutor {
+
+ HttpResponseTemplate execute(HttpRequestTemplate httpRequestTemplate) throws IOException;
+}
diff --git a/src/main/java/org/springframework/feign/invoke/FeignMethodHandler.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/DefaultMethodInvoker.java
similarity index 70%
rename from src/main/java/org/springframework/feign/invoke/FeignMethodHandler.java
rename to src/main/java/com/cowave/commons/client/http/invoke/proxy/DefaultMethodInvoker.java
index 7194c77..80a0e11 100644
--- a/src/main/java/org/springframework/feign/invoke/FeignMethodHandler.java
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/DefaultMethodInvoker.java
@@ -1,6 +1,4 @@
-package org.springframework.feign.invoke;
-
-import feign.InvocationHandlerFactory;
+package com.cowave.commons.client.http.invoke.proxy;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -12,22 +10,22 @@
* @author shanhuiming
*
*/
-public class FeignMethodHandler implements InvocationHandlerFactory.MethodHandler {
+public class DefaultMethodInvoker implements MethodInvoker {
+
// Uses Java 7 MethodHandle based reflection. As default methods will only exist when
// run on a Java 8 JVM this will not affect use on legacy JVMs.
- // When Feign upgrades to Java 7, remove the @IgnoreJRERequirement annotation.
+ // When upgrades to Java 7, remove the @IgnoreJRERequirement annotation.
private final MethodHandle unboundHandle;
// handle is effectively final after bindTo has been called.
private MethodHandle handle;
- public FeignMethodHandler(Method defaultMethod) {
+ public DefaultMethodInvoker(Method defaultMethod) {
try {
Class> declaringClass = defaultMethod.getDeclaringClass();
Field field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
field.setAccessible(true);
MethodHandles.Lookup lookup = (MethodHandles.Lookup) field.get(null);
-
this.unboundHandle = lookup.unreflectSpecial(defaultMethod, declaringClass);
} catch (NoSuchFieldException ex) {
throw new IllegalStateException(ex);
@@ -36,10 +34,6 @@ public FeignMethodHandler(Method defaultMethod) {
}
}
- /**
- * Bind this handler to a proxy object. After bound, DefaultMethodHandler#invoke will act as if it was called
- * on the proxy object. Must be called once and only once for a given instance of DefaultMethodHandler
- */
public void bindTo(Object proxy) {
if(handle != null) {
throw new IllegalStateException("Attempted to rebind a default method handler that was already bound");
@@ -47,10 +41,6 @@ public void bindTo(Object proxy) {
handle = unboundHandle.bindTo(proxy);
}
- /**
- * Invoke this method. DefaultMethodHandler#bindTo must be called before the first
- * time invoke is called.
- */
@Override
public Object invoke(Object[] argv) throws Throwable {
if(handle == null) {
@@ -58,4 +48,4 @@ public Object invoke(Object[] argv) throws Throwable {
}
return handle.invokeWithArguments(argv);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/proxy/HttpMethodInvoker.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/HttpMethodInvoker.java
new file mode 100644
index 0000000..6ce94c2
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/HttpMethodInvoker.java
@@ -0,0 +1,230 @@
+package com.cowave.commons.client.http.invoke.proxy;
+
+import com.cowave.commons.client.http.HttpInterceptor;
+import com.cowave.commons.client.http.invoke.codec.decoder.JacksonDecoder;
+import com.cowave.commons.client.http.invoke.exec.HttpExecutor;
+import com.cowave.commons.client.http.response.HttpResponse;
+import com.cowave.commons.client.http.response.HttpResponseTemplate;
+import com.cowave.commons.client.http.request.Options;
+import com.cowave.commons.client.http.request.HttpRequestTemplate;
+import com.cowave.commons.client.http.request.HttpRequest;
+import com.cowave.commons.client.http.request.HttpRequestFactory;
+import com.cowave.commons.client.http.request.meta.HttpMethodMeta;
+import com.cowave.commons.client.http.asserts.HttpException;
+import com.cowave.commons.client.http.asserts.HttpHintException;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.event.Level;
+import com.cowave.commons.client.http.HttpExceptionHandler;
+import com.cowave.commons.client.http.invoke.codec.HttpDecoder;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.StreamUtils;
+import org.springframework.util.StringUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import static com.cowave.commons.client.http.HttpCode.SERVICE_ERROR;
+import static org.slf4j.event.Level.WARN;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Slf4j
+public class HttpMethodInvoker implements MethodInvoker {
+ private final Level level;
+ private final boolean ignoreError;
+ private final HttpMethodMeta metadata;
+ private final ProxyTarget> proxyTarget;
+ private final HttpExecutor httpExecutor;
+ private final List httpInterceptors;
+ private final HttpRequestFactory httpRequestFactory;
+ private final Options options;
+ private final HttpDecoder decoder;
+ private final HttpExceptionHandler exceptionHandler;
+
+ public HttpMethodInvoker(ProxyTarget> proxyTarget,
+ HttpMethodMeta metadata,
+ HttpRequestFactory httpRequestFactory,
+ HttpExecutor httpExecutor,
+ List httpInterceptors,
+ Options options,
+ HttpDecoder decoder,
+ Level level,
+ boolean ignoreError,
+ HttpExceptionHandler exceptionHandler) {
+ this.proxyTarget = proxyTarget;
+ this.httpExecutor = httpExecutor;
+ this.httpInterceptors = httpInterceptors;
+ this.metadata = metadata;
+ this.httpRequestFactory = httpRequestFactory;
+ this.options = options;
+ this.decoder = decoder;
+ this.level = level;
+ this.ignoreError = ignoreError;
+ this.exceptionHandler = exceptionHandler;
+ }
+
+ @Override
+ public Object invoke(Object[] args) throws Throwable {
+ HttpRequest httpRequest = httpRequestFactory.create(args);
+ return executeAndDecode(httpRequest);
+ }
+
+ Object executeAndDecode(HttpRequest httpRequest) throws Throwable {
+ Type returnType = metadata.getReturnType();
+ Type httpType = getParamTypeOf(returnType, HttpResponse.class);
+
+ // Http请求
+ HttpRequestTemplate httpRequestTemplate = applyTemplate(httpRequest);
+ String url = httpRequestTemplate.getUrl();
+ String method = httpRequestTemplate.getMethod();
+
+ long start = System.nanoTime();
+ HttpResponseTemplate httpResponseTemplate;
+ try {
+ httpResponseTemplate = httpExecutor.execute(httpRequestTemplate);
+ } catch (IOException e) {
+ log.error(">< {} {} {}", e.getMessage(), method, url, e);
+ return throwOrReturn(httpType, new HttpHintException("Remote failed"));
+ }
+
+ long cost = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
+ int status = httpResponseTemplate.getStatus();
+ try {
+ // 3.响应类型: org.springframework.feign.invoke.codec.HttpResponse
+ if (httpType != null) {
+ return parseHttpResponse(httpType, httpResponseTemplate, status, method, url, cost);
+ }
+
+ // 4.decoder解码
+ if (status == 200) {
+ return decoder.decode(httpResponseTemplate, metadata.getReturnType(), url, cost, status, level);
+ }
+
+ if (httpResponseTemplate.getInputStream() != null) {
+ String body = StreamUtils.copyToString(httpResponseTemplate.getInputStream(), StandardCharsets.UTF_8);
+ log.error(">< {} {}ms {} {} {}", status, cost, method, url, body);
+ } else {
+ log.error(">< {} {}ms {} {}", status, cost, method, url);
+ }
+ throw new HttpHintException(status, SERVICE_ERROR.getCode(), "Remote failed");
+ } catch (HttpException e) {
+ if (exceptionHandler != null) {
+ exceptionHandler.handle(e);
+ }
+ return throwOrReturn(httpType, e);
+ } catch (Exception e) {
+ log.error(">< {}ms {} {} {}", cost, e.getMessage(), method, url, e);
+ return throwOrReturn(httpType, new HttpHintException("Remote failed", e));
+ }
+ }
+
+ private Object throwOrReturn(Type httpType, Exception exception) throws Throwable {
+ if (ignoreError && httpType != null) {
+ HttpResponse> httpResponse = new HttpResponse<>(SERVICE_ERROR);
+ httpResponse.setCause(exception);
+ httpResponse.setMessage(exception.getMessage());
+ return httpResponse;
+ }
+ throw exception;
+ }
+
+ private HttpResponse> parseHttpResponse(Type paramType, HttpResponseTemplate response,
+ int status, String method, String url, long cost) throws IOException {
+ // Header信息
+ HttpHeaders headers = new HttpHeaders();
+ for (Map.Entry> entry : response.getHeaders().entrySet()) {
+ headers.put(entry.getKey(), new ArrayList<>(entry.getValue()));
+ }
+
+ // InputStream交给调用者处理
+ if (paramType.equals(InputStream.class)) {
+ if (status == 200) {
+ if (log.isDebugEnabled() || level.toInt() < WARN.toInt()) {
+ log.info(">< {} {}ms {} {}", status, cost, method, url);
+ }
+ } else {
+ log.warn(">< {} {}ms {}, {}", status, cost, method, url);
+ }
+ return new HttpResponse<>(response.getStatus(), headers, response.getInputStream());
+ }
+
+ // 获取响应主体
+ String body = null;
+ if (response.getInputStream() != null) {
+ body = StreamUtils.copyToString(response.getInputStream(), StandardCharsets.UTF_8);
+ }
+
+ if (status == 200) {
+ if (log.isDebugEnabled() || level.toInt() < WARN.toInt()) {
+ log.info(">< {} {}ms {} {}", status, cost, method, url);
+ }
+ if (!StringUtils.hasText(body) || paramType.equals(String.class) || paramType.equals(Void.class)) {
+ return new HttpResponse<>(response.getStatus(), headers, body);
+ } else {
+ return new HttpResponse<>(response.getStatus(), headers, JacksonDecoder.readValue(body, paramType));
+ }
+ } else if (status > 200 && status < 300) {
+ log.warn(">< {} {}ms {} {} {}", status, cost, method, url, body);
+ HttpResponse> httpResponse = new HttpResponse<>(response.getStatus(), headers, null);
+ httpResponse.setMessage(body);
+ return httpResponse;
+ } else {
+ log.error(">< {} {}ms {} {} {}", status, cost, method, url, body);
+ HttpResponse> httpResponse = new HttpResponse<>(response.getStatus(), headers, null);
+ httpResponse.setMessage(body);
+ return httpResponse;
+ }
+ }
+
+ private Type getParamTypeOf(Type type, Class> clazz) {
+ if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ Type rawType = parameterizedType.getRawType();
+ if (rawType instanceof Class> && clazz.equals(rawType)) {
+ Type[] paramTypes = parameterizedType.getActualTypeArguments();
+ if (paramTypes == null || paramTypes.length == 0) {
+ return Object.class;
+ } else {
+ return paramTypes[0];
+ }
+ }
+ }
+ return null;
+ }
+
+ HttpRequestTemplate applyTemplate(HttpRequest httpRequest) throws UnsupportedEncodingException {
+ for (HttpInterceptor interceptor : httpInterceptors) {
+ interceptor.apply(httpRequest);
+ }
+
+ // 方法Options注解指定的超时优先级更高
+ int connectTimeout = options.getConnectTimeout();
+ int readTimeout = options.getReadTimeout();
+ int retryTimes = options.getRetryTimes();
+ int retryInterval = options.getRetryInterval();
+ if (metadata.getConnectTimeout() != -1) {
+ connectTimeout = metadata.getConnectTimeout();
+ }
+ if (metadata.getReadTimeout() != -1) {
+ readTimeout = metadata.getReadTimeout();
+ }
+ if (metadata.getRetryTimes() != -1) {
+ retryTimes = metadata.getRetryTimes();
+ }
+ if (metadata.getRetryInterval() != -1) {
+ retryInterval = metadata.getRetryInterval();
+ }
+ // 设置url及一些参数
+ return proxyTarget.apply(httpRequest,
+ httpRequest.getHostUrl(), retryTimes, retryInterval, connectTimeout, readTimeout);
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/proxy/HttpMethodInvokerFactory.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/HttpMethodInvokerFactory.java
new file mode 100644
index 0000000..e88aaf5
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/HttpMethodInvokerFactory.java
@@ -0,0 +1,65 @@
+package com.cowave.commons.client.http.invoke.proxy;
+
+import com.cowave.commons.client.http.request.MultipartRequestFactory;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.event.Level;
+import com.cowave.commons.client.http.HttpExceptionHandler;
+import com.cowave.commons.client.http.HttpInterceptor;
+import com.cowave.commons.client.http.invoke.codec.HttpDecoder;
+import com.cowave.commons.client.http.invoke.codec.HttpEncoder;
+import com.cowave.commons.client.http.invoke.exec.HttpExecutor;
+import com.cowave.commons.client.http.request.Options;
+import com.cowave.commons.client.http.request.meta.HttpMethodMetaParser;
+import com.cowave.commons.client.http.request.meta.HttpMethodMeta;
+import com.cowave.commons.client.http.request.HttpRequestFactory;
+import com.cowave.commons.client.http.request.BodyRequestFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@RequiredArgsConstructor
+public class HttpMethodInvokerFactory {
+ private final HttpExecutor httpExecutor;
+ private final HttpMethodMetaParser metaParser;
+ private final HttpEncoder encoder;
+ private final HttpDecoder decoder;
+ private final Options options;
+ private final List httpInterceptors;
+ private final HttpExceptionHandler exceptionHandler;
+
+ private final boolean ignoreError;
+ private final Level level;
+
+ public Map create(ProxyTarget> proxyTarget) throws UnsupportedEncodingException {
+
+ List metaList = metaParser.parse(proxyTarget.type());
+
+ Map result = new LinkedHashMap<>();
+ for (HttpMethodMeta meta : metaList) {
+ HttpRequestFactory httpRequestFactory;
+ if (meta.getMultipartFileIndex() != null
+ || meta.getMultipartFormIndex() != null || !meta.getMultipartParams().isEmpty()) {
+ // multipart/form-data
+ httpRequestFactory = new MultipartRequestFactory(meta);
+ } else if (meta.getBodyIndex() != null) {
+ // 存在body
+ httpRequestFactory = new BodyRequestFactory(meta, encoder);
+ } else {
+ httpRequestFactory = new HttpRequestFactory(meta);
+ }
+
+ //
+ result.put(meta.getMethodKey(),
+ new HttpMethodInvoker(proxyTarget, meta, httpRequestFactory,
+ httpExecutor, httpInterceptors, options, decoder, level, ignoreError, exceptionHandler));
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/proxy/MethodInvoker.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/MethodInvoker.java
new file mode 100644
index 0000000..d22ca76
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/MethodInvoker.java
@@ -0,0 +1,11 @@
+package com.cowave.commons.client.http.invoke.proxy;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public interface MethodInvoker {
+
+ Object invoke(Object[] argv) throws Throwable;
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyFactory.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyFactory.java
new file mode 100644
index 0000000..4ef5883
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyFactory.java
@@ -0,0 +1,58 @@
+package com.cowave.commons.client.http.invoke.proxy;
+
+import com.cowave.commons.client.http.request.meta.HttpMethodMetaParser;
+
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class ProxyFactory {
+
+ private final HttpMethodInvokerFactory httpMethodInvokerFactory;
+
+ public ProxyFactory(HttpMethodInvokerFactory httpMethodInvokerFactory) {
+ this.httpMethodInvokerFactory = httpMethodInvokerFactory;
+ }
+
+ @SuppressWarnings("unchecked")
+ public T newProxy(ProxyTarget proxyTarget) throws UnsupportedEncodingException {
+ // 默认方法
+ List defaultProxyInvokerList = new LinkedList<>();
+
+ // Http调用方法
+ Map httpProxyInvokerMap = httpMethodInvokerFactory.create(proxyTarget);
+
+ Map methodInvokeHandlerMap = new LinkedHashMap<>();
+ for (Method method : proxyTarget.type().getMethods()) {
+ if (method.getDeclaringClass() == Object.class) {
+ // object方法不进行代理
+ } else if (HttpMethodMetaParser.isDefault(method)) {
+ DefaultMethodInvoker handler = new DefaultMethodInvoker(method);
+ defaultProxyInvokerList.add(handler);
+ methodInvokeHandlerMap.put(method, handler);
+ } else {
+ methodInvokeHandlerMap.put(
+ method, httpProxyInvokerMap.get(HttpMethodMetaParser.methodKey(proxyTarget.type(), method)));
+ }
+ }
+
+ // 创建代理
+ T proxy = (T) Proxy.newProxyInstance(
+ proxyTarget.type().getClassLoader(),
+ new Class>[]{proxyTarget.type()},
+ new ProxyInvoker(proxyTarget, methodInvokeHandlerMap));
+ for (DefaultMethodInvoker methodHandler : defaultProxyInvokerList) {
+ methodHandler.bindTo(proxy);
+ }
+ return proxy;
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyFactoryBuilder.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyFactoryBuilder.java
new file mode 100644
index 0000000..bb8b451
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyFactoryBuilder.java
@@ -0,0 +1,46 @@
+package com.cowave.commons.client.http.invoke.proxy;
+
+import com.cowave.commons.client.http.request.meta.HttpMethodMetaParser;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.event.Level;
+import com.cowave.commons.client.http.HttpExceptionHandler;
+import com.cowave.commons.client.http.HttpInterceptor;
+import com.cowave.commons.client.http.invoke.codec.HttpDecoder;
+import com.cowave.commons.client.http.invoke.codec.HttpEncoder;
+import com.cowave.commons.client.http.invoke.codec.decoder.JacksonDecoder;
+import com.cowave.commons.client.http.invoke.codec.encoder.JacksonEncoder;
+import com.cowave.commons.client.http.invoke.exec.HttpExecutor;
+import com.cowave.commons.client.http.request.Options;
+import com.cowave.commons.client.http.request.meta.HttpMethodMetaParserImpl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Data
+@RequiredArgsConstructor
+public class ProxyFactoryBuilder {
+ private final HttpMethodMetaParser metaParser = new HttpMethodMetaParserImpl();
+ private final List httpInterceptors = new ArrayList<>();
+ private final Options options;
+ private HttpEncoder encoder = new JacksonEncoder();
+ private HttpDecoder decoder = new JacksonDecoder();
+ private HttpExecutor httpExecutor;
+ private HttpExceptionHandler exceptionHandler;
+
+ public void addHttpInterceptor(HttpInterceptor httpInterceptor) {
+ this.httpInterceptors.add(httpInterceptor);
+ }
+
+ public ProxyFactory newProxyFactory(Level level, boolean ignoreError) {
+ HttpMethodInvokerFactory methodHandlerFactory = new HttpMethodInvokerFactory(
+ httpExecutor, metaParser, encoder, decoder,
+ options, httpInterceptors, exceptionHandler, ignoreError, level);
+ return new ProxyFactory(methodHandlerFactory);
+ }
+}
diff --git a/src/main/java/org/springframework/feign/invoke/FeignInvocationHandler.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyInvoker.java
similarity index 56%
rename from src/main/java/org/springframework/feign/invoke/FeignInvocationHandler.java
rename to src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyInvoker.java
index 4e5173e..c03a6a3 100644
--- a/src/main/java/org/springframework/feign/invoke/FeignInvocationHandler.java
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyInvoker.java
@@ -1,30 +1,26 @@
-package org.springframework.feign.invoke;
-
-import feign.*;
+package com.cowave.commons.client.http.invoke.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
-import static feign.Util.checkNotNull;
-
/**
*
* @author shanhuiming
*
*/
-public class FeignInvocationHandler implements InvocationHandler {
+public class ProxyInvoker implements InvocationHandler {
@SuppressWarnings("rawtypes")
- private final Target target;
-
- private final Map dispatch;
+ private final ProxyTarget proxyTarget;
+
+ private final Map methodInvokeHandlerMap;
@SuppressWarnings("rawtypes")
- FeignInvocationHandler(Target target, Map dispatch) {
- this.target = checkNotNull(target, "target");
- this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
+ ProxyInvoker(ProxyTarget proxyTarget, Map methodInvokeHandlerMap) {
+ this.proxyTarget = proxyTarget;
+ this.methodInvokeHandlerMap = methodInvokeHandlerMap;
}
@Override
@@ -41,25 +37,25 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl
} else if ("toString".equals(method.getName())) {
return toString();
}
- return dispatch.get(method).invoke(args);
+ return methodInvokeHandlerMap.get(method).invoke(args);
}
@Override
public boolean equals(Object obj) {
- if (obj instanceof FeignInvocationHandler) {
- FeignInvocationHandler other = (FeignInvocationHandler) obj;
- return target.equals(other.target);
+ if (obj instanceof ProxyInvoker) {
+ ProxyInvoker other = (ProxyInvoker) obj;
+ return proxyTarget.equals(other.proxyTarget);
}
return false;
}
@Override
public int hashCode() {
- return target.hashCode();
+ return proxyTarget.hashCode();
}
@Override
public String toString() {
- return target.toString();
+ return proxyTarget.toString();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyTarget.java b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyTarget.java
new file mode 100644
index 0000000..55d5c42
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/invoke/proxy/ProxyTarget.java
@@ -0,0 +1,101 @@
+package com.cowave.commons.client.http.invoke.proxy;
+
+import com.cowave.commons.client.http.asserts.HttpHintException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContext;
+import com.cowave.commons.client.http.HttpServiceChooser;
+import com.cowave.commons.client.http.request.HttpRequestTemplate;
+import com.cowave.commons.client.http.request.HttpRequest;
+import org.springframework.util.StringUtils;
+import org.springframework.util.StringValueResolver;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class ProxyTarget {
+ private final ApplicationContext applicationContext;
+ private final StringValueResolver valueResolver;
+ private final Class type;
+ private final String name;
+ private final String url;
+
+ public HttpRequestTemplate apply(HttpRequest httpRequest, String hostUrl,
+ int retryTimes, int retryInterval, int connectTimeout, int readTimeout) throws UnsupportedEncodingException {
+ if(StringUtils.hasText(hostUrl)){
+ httpRequest.insert(0, hostUrl);
+ return httpRequest.requestTemplate(retryTimes, retryInterval, connectTimeout, readTimeout);
+ }else{
+ return apply(httpRequest, retryTimes, retryInterval, connectTimeout, readTimeout);
+ }
+ }
+
+ public HttpRequestTemplate apply(HttpRequest httpRequest,
+ int retryTimes, int retryInterval, int connectTimeout, int readTimeout) throws UnsupportedEncodingException {
+ String prasedName = "";
+ if(StringUtils.hasText(name)){
+ HttpServiceChooser serviceChooser = applicationContext.getBean(HttpServiceChooser.class);
+ prasedName = serviceChooser.choose(name);
+ }
+
+ String prasedUrl = url;
+ if(StringUtils.hasText(url) && url.contains("${")){
+ prasedUrl = valueResolver.resolveStringValue(url);
+ }
+
+ String parsed = prasedName + prasedUrl;
+ if (httpRequest.url().indexOf("http") != 0) {
+ if(parsed.indexOf("http") != 0){
+ log.error(">< Remote failed due to illegal url, {}", parsed);
+ throw new HttpHintException("Remote failed");
+ }
+ httpRequest.insert(0, parsed);
+ }
+ return httpRequest.requestTemplate(retryTimes, retryInterval, connectTimeout, readTimeout);
+ }
+
+ public Class type() {
+ return type;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String url() {
+ return url;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ ProxyTarget> other = (ProxyTarget>) obj;
+ return type.equals(other.type) && name.equals(other.name) && url.equals(other.url);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 3 * result + type.hashCode();
+ result = 5 * result + name.hashCode();
+ result = 7 * result + url.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "{type=" + type.getSimpleName() + ", name=" + name + ", url=" + url + "}";
+ }
+}
diff --git a/src/main/java/org/springframework/feign/FeignBeanDefinitionRegistrar.java b/src/main/java/com/cowave/commons/client/http/register/HttpClientBeanDefinitionRegistrar.java
similarity index 78%
rename from src/main/java/org/springframework/feign/FeignBeanDefinitionRegistrar.java
rename to src/main/java/com/cowave/commons/client/http/register/HttpClientBeanDefinitionRegistrar.java
index 1420dce..1625ee2 100644
--- a/src/main/java/org/springframework/feign/FeignBeanDefinitionRegistrar.java
+++ b/src/main/java/com/cowave/commons/client/http/register/HttpClientBeanDefinitionRegistrar.java
@@ -1,5 +1,6 @@
-package org.springframework.feign;
+package com.cowave.commons.client.http.register;
+import com.cowave.commons.client.http.annotation.HttpClient;
import org.reflections.Reflections;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -11,7 +12,6 @@
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata;
-import org.springframework.feign.annotation.FeignClient;
import org.springframework.lang.NonNull;
import java.util.*;
@@ -21,36 +21,41 @@
* @author shanhuiming
*
*/
-public class FeignBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
+public class HttpClientBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata meta, @NonNull BeanDefinitionRegistry registry) {
TreeSet packageSet = new TreeSet<>();
String[] beanNames = registry.getBeanDefinitionNames();
+
+ // 省掉package指定扫描,就检查下启动类下的路径,以及@ComponentScan指定的路径
try {
for (String beanName : beanNames) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
String className = beanDefinition.getBeanClassName();
if (className != null) {
Class> clazz = Class.forName(className);
+
// 启动类路径
SpringBootApplication springBoot = clazz.getAnnotation(SpringBootApplication.class);
if (springBoot != null) {
packageSet.add(clazz.getPackage().getName());
}
+
// 扫描类路径
ComponentScan componentScan = clazz.getAnnotation(ComponentScan.class);
if (componentScan != null) {
- packageSet.addAll(List.of(componentScan.basePackages()));
+ packageSet.addAll(Arrays.asList(componentScan.basePackages()));
}
}
}
} catch (ClassNotFoundException e) {
throw new ApplicationContextException("", e);
}
- List packageList = packageSet.stream().toList();
- // 去下重
+ List packageList = new ArrayList<>(packageSet);
+
+ // 合并package
List packages = new ArrayList<>();
for(int right = packageList.size() - 1; right >= 0; right--){
boolean hasPrefix = false;
@@ -69,16 +74,17 @@ public void registerBeanDefinitions(AnnotationMetadata meta, @NonNull BeanDefini
// 扫描注册
for(String pack : packages){
Reflections reflections = new Reflections(pack);
- for(Class> clazz : reflections.getTypesAnnotatedWith(FeignClient.class)){
- FeignClient feign = AnnotationUtils.getAnnotation(clazz, FeignClient.class);
+ for(Class> clazz : reflections.getTypesAnnotatedWith(HttpClient.class)){
+ HttpClient feign = AnnotationUtils.getAnnotation(clazz, HttpClient.class);
if(feign == null){
continue;
}
+
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition)builder.getBeanDefinition();
beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
- beanDefinition.getPropertyValues().add("feignClass", clazz);
- beanDefinition.setBeanClass(FeignFactory.class);
+ beanDefinition.getPropertyValues().add("targetClass", clazz);
+ beanDefinition.setBeanClass(HttpClientFactoryBean.class);
registry.registerBeanDefinition(clazz.getSimpleName(), beanDefinition);
}
}
diff --git a/src/main/java/com/cowave/commons/client/http/register/HttpClientFactoryBean.java b/src/main/java/com/cowave/commons/client/http/register/HttpClientFactoryBean.java
new file mode 100644
index 0000000..3babbc6
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/register/HttpClientFactoryBean.java
@@ -0,0 +1,50 @@
+package com.cowave.commons.client.http.register;
+
+import com.cowave.commons.client.http.annotation.HttpClient;
+import lombok.Data;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.EmbeddedValueResolverAware;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.lang.NonNull;
+import org.springframework.util.StringValueResolver;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+@Data
+public class HttpClientFactoryBean implements FactoryBean, EmbeddedValueResolverAware, ApplicationContextAware {
+
+ private Class targetClass;
+
+ public HttpClientFactoryBean() {
+
+ }
+
+ @Override
+ public T getObject() throws UnsupportedEncodingException {
+ HttpClient httpClient = AnnotationUtils.getAnnotation(targetClass, HttpClient.class);
+ return HttpProxyFactory.newProxy(targetClass, httpClient);
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return targetClass;
+ }
+
+ @Override
+ public void setEmbeddedValueResolver(@NonNull StringValueResolver valueResolver) {
+ HttpProxyFactory.setStringValueResolver(valueResolver);
+ }
+
+ @Override
+ public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
+ HttpProxyFactory.setApplicationContext(applicationContext);
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/register/HttpProxyFactory.java b/src/main/java/com/cowave/commons/client/http/register/HttpProxyFactory.java
new file mode 100644
index 0000000..74d2327
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/register/HttpProxyFactory.java
@@ -0,0 +1,88 @@
+package com.cowave.commons.client.http.register;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Objects;
+
+import com.cowave.commons.client.http.HttpInterceptor;
+import com.cowave.commons.client.http.annotation.HttpClient;
+import com.cowave.commons.client.http.invoke.exec.HttpClientExecutor;
+import com.cowave.commons.client.http.invoke.codec.HttpEncoder;
+import com.cowave.commons.client.http.invoke.proxy.ProxyFactory;
+import com.cowave.commons.client.http.invoke.proxy.ProxyFactoryBuilder;
+import com.cowave.commons.client.http.invoke.proxy.ProxyTarget;
+import com.cowave.commons.client.http.request.Options;
+import org.springframework.context.ApplicationContext;
+import com.cowave.commons.client.http.HttpExceptionHandler;
+import com.cowave.commons.client.http.invoke.codec.HttpDecoder;
+import org.springframework.util.StringUtils;
+import org.springframework.util.StringValueResolver;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+class HttpProxyFactory {
+
+ private static ApplicationContext applicationContext;
+
+ private static StringValueResolver valueResolver;
+
+ static void setApplicationContext(ApplicationContext applicationContext) {
+ HttpProxyFactory.applicationContext = applicationContext;
+ }
+
+ static void setStringValueResolver(StringValueResolver valueResolver) {
+ HttpProxyFactory.valueResolver = valueResolver;
+ }
+
+ static T newProxy(Class clazz, HttpClient httpClient) throws UnsupportedEncodingException {
+ int retryTimes = getInt(httpClient.retryTimes(), httpClient.retryTimesStr());
+ int retryInterval = getInt(httpClient.retryInterval(), httpClient.retryIntervalStr());
+ int connectTimeout = getInt(httpClient.connectTimeout(), httpClient.connectTimeoutStr());
+ int readTimeout = getInt(httpClient.readTimeout(), httpClient.readTimeoutStr());
+ ProxyFactoryBuilder proxyFactoryBuilder = new ProxyFactoryBuilder(new Options(connectTimeout, readTimeout, retryTimes, retryInterval));
+
+ String[] interceptors = applicationContext.getBeanNamesForType(HttpInterceptor.class);
+ for (String interceptorName : interceptors) {
+ HttpInterceptor httpInterceptor = applicationContext.getBean(interceptorName, HttpInterceptor.class);
+ proxyFactoryBuilder.addHttpInterceptor(httpInterceptor);
+ }
+
+ String[] handlers = applicationContext.getBeanNamesForType(HttpExceptionHandler.class);
+ if (handlers.length > 0) {
+ String handler = handlers[0];
+ HttpExceptionHandler exceptionHandler = applicationContext.getBean(handler, HttpExceptionHandler.class);
+ proxyFactoryBuilder.setExceptionHandler(exceptionHandler);
+ }
+
+ try {
+ // encode / decoder
+ proxyFactoryBuilder.setEncoder((HttpEncoder) httpClient.encoder().newInstance());
+ proxyFactoryBuilder.setDecoder((HttpDecoder) httpClient.decoder().newInstance());
+ // Https设置
+ Class extends SSLSocketFactory> sslSocketFactoryClass = httpClient.sslSocketFactory();
+ Class extends HostnameVerifier> hostnameVerifierClass = httpClient.hostnameVerifier();
+ SSLSocketFactory SSLSocketFactory = sslSocketFactoryClass.newInstance();
+ HostnameVerifier hostnameVerifier = hostnameVerifierClass.newInstance();
+ proxyFactoryBuilder.setHttpExecutor(new HttpClientExecutor(SSLSocketFactory, hostnameVerifier));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ ProxyFactory proxyFactory = proxyFactoryBuilder.newProxyFactory(httpClient.level(), httpClient.ignoreError());
+ ProxyTarget proxyTarget = new ProxyTarget<>(
+ applicationContext, valueResolver, clazz, httpClient.name(), httpClient.url());
+ return proxyFactory.newProxy(proxyTarget);
+ }
+
+ private static int getInt(int defaultValue, String regex) {
+ if (!StringUtils.hasText(regex)) {
+ return defaultValue;
+ }
+ return Integer.parseInt(Objects.requireNonNull(valueResolver.resolveStringValue(regex)));
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/request/BodyRequestFactory.java b/src/main/java/com/cowave/commons/client/http/request/BodyRequestFactory.java
new file mode 100644
index 0000000..cf40dfe
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/request/BodyRequestFactory.java
@@ -0,0 +1,39 @@
+package com.cowave.commons.client.http.request;
+
+import com.cowave.commons.client.http.request.meta.HttpMethodMeta;
+import com.cowave.commons.client.http.invoke.codec.HttpEncoder;
+
+import java.util.Collection;
+import java.util.Map;
+
+import static com.cowave.commons.client.http.HttpHeader.Content_Type;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class BodyRequestFactory extends HttpRequestFactory {
+
+ private final HttpEncoder encoder;
+
+ public BodyRequestFactory(HttpMethodMeta metadata, HttpEncoder encoder) {
+ super(metadata);
+ this.encoder = encoder;
+ }
+
+ @Override
+ protected HttpRequest resolve(Object[] argv, HttpRequest httpRequest,
+ Map variables, Map multiParams) throws Exception {
+ Object body = argv[metadata.getBodyIndex()];
+
+ encoder.encode(httpRequest, body);
+
+ // body请求,默认设置下application/json
+ Map> headers = httpRequest.headers();
+ if(!headers.containsKey(Content_Type)){
+ httpRequest.header(Content_Type, "application/json");
+ }
+ return super.resolve(argv, httpRequest, variables, multiParams);
+ }
+}
diff --git a/src/main/java/com/cowave/commons/client/http/request/HttpRequest.java b/src/main/java/com/cowave/commons/client/http/request/HttpRequest.java
new file mode 100644
index 0000000..58b47c6
--- /dev/null
+++ b/src/main/java/com/cowave/commons/client/http/request/HttpRequest.java
@@ -0,0 +1,626 @@
+package com.cowave.commons.client.http.request;
+
+import com.cowave.commons.client.http.asserts.Asserts;
+import org.springframework.util.StringUtils;
+
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Array;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+import static com.cowave.commons.client.http.request.HttpRequestTemplate.valuesOrEmpty;
+
+/**
+ *
+ * @author shanhuiming
+ *
+ */
+public class HttpRequest {
+ private final Map> queries = new LinkedHashMap<>();
+ private final Map> headers = new LinkedHashMap<>();
+ private StringBuilder url = new StringBuilder();
+ private String method;
+ private byte[] body;
+ private Map multiForm;
+ private InputStream multiFile;
+ private String multiFileName;
+ private String bodyTemplate;
+ private transient Charset charset;
+ private boolean decodeSlash = true;
+ // 调用时指定的url
+ private String hostUrl;
+
+ public HttpRequest() {
+
+ }
+
+ public HttpRequest(HttpRequest copy) {
+ this.queries.putAll(copy.queries);
+ this.headers.putAll(copy.headers);
+ this.url.append(copy.url);
+ this.method = copy.method;
+ this.body = copy.body;
+ this.bodyTemplate = copy.bodyTemplate;
+ this.charset = copy.charset;
+ this.decodeSlash = copy.decodeSlash;
+ this.hostUrl = copy.hostUrl;
+ }
+
+ public HttpRequestTemplate requestTemplate(int retryTimes, int retryInterval, int connectTimeout, int readTimeout) {
+ Map> safeCopy = new LinkedHashMap<>(headers);
+ return new HttpRequestTemplate(method, url + queryLine(), body, charset,
+ Collections.unmodifiableMap(safeCopy),
+ connectTimeout, readTimeout, retryTimes, retryInterval,
+ multiFile, multiFileName, multiForm);
+ }
+
+ void setHostUrl(String hostUrl) {
+ this.hostUrl = hostUrl;
+ }
+
+ public String getHostUrl() {
+ return hostUrl;
+ }
+
+ void setMultiFile(InputStream multiFile) {
+ this.multiFile = multiFile;
+ }
+
+ void setMultiFileName(String multiFileName) {
+ this.multiFileName = multiFileName;
+ }
+
+ void setMultiForm(Map multiForm) {
+ this.multiForm = multiForm;
+ }
+
+ private static String urlDecode(String arg) throws UnsupportedEncodingException {
+ return URLDecoder.decode(arg, "UTF-8");
+ }
+
+ private static String urlEncode(Object arg) throws UnsupportedEncodingException {
+ return URLEncoder.encode(String.valueOf(arg), "UTF-8");
+ }
+
+ private static boolean isHttpUrl(CharSequence value) {
+ return value.length() >= 4 && value.subSequence(0, 3).equals("http".substring(0, 3));
+ }
+
+ private static CharSequence removeTrailingSlash(CharSequence charSequence) {
+ if (charSequence != null && charSequence.length() > 0 && charSequence.charAt(charSequence.length() - 1) == '/') {
+ return charSequence.subSequence(0, charSequence.length() - 1);
+ } else {
+ return charSequence;
+ }
+ }
+
+ /**
+ * Expands a {@code template}, such as {@code username}, using the {@code variables} supplied. Any
+ * unresolved parameters will remain. Note that if you'd like curly braces literally in the
+ * {@code template}, urlencode them first.
+ *
+ * @param template URI template that can be in level 1 RFC6570
+ * form.
+ * @param variables to the URI template
+ * @return expanded template, leaving any unresolved parameters literal
+ */
+ public static String expand(String template, Map variables) {
+ // skip expansion if there's no valid variables set. ex. {a} is the
+ // first valid
+ Asserts.notNull(template, "template can't be null");
+ Asserts.notNull(variables, "variables for " + template + " can't be null");
+ if (template.length() < 3) {
+ return template;
+ }
+
+ boolean inVar = false;
+ StringBuilder var = new StringBuilder();
+ StringBuilder builder = new StringBuilder();
+ for (char c : template.toCharArray()) {
+ switch (c) {
+ case '{':
+ if (inVar) {
+ // '{{' is an escape: write the brace and don't interpret as a variable
+ builder.append("{");
+ inVar = false;
+ break;
+ }
+ inVar = true;
+ break;
+ case '}':
+ if (!inVar) { // then write the brace literally
+ builder.append('}');
+ break;
+ }
+ inVar = false;
+ String key = var.toString();
+ Object value = variables.get(var.toString());
+ if (value != null) {
+ builder.append(value);
+ } else {
+ builder.append('{').append(key).append('}');
+ }
+ var = new StringBuilder();
+ break;
+ default:
+ if (inVar) {
+ var.append(c);
+ } else {
+ builder.append(c);
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ private static Map> parseAndDecodeQueries(String queryLine) throws UnsupportedEncodingException {
+ Map> map = new LinkedHashMap<>();
+ if (!StringUtils.hasText(queryLine)) {
+ return map;
+ }
+ if (queryLine.indexOf('&') == -1) {
+ putKV(queryLine, map);
+ } else {
+ char[] chars = queryLine.toCharArray();
+ int start = 0;
+ int i = 0;
+ for (; i < chars.length; i++) {
+ if (chars[i] == '&') {
+ putKV(queryLine.substring(start, i), map);
+ start = i + 1;
+ }
+ }
+ putKV(queryLine.substring(start, i), map);
+ }
+ return map;
+ }
+
+ private static void putKV(String stringToParse, Map> map) throws UnsupportedEncodingException {
+ String key;
+ String value;
+ // note that '=' can be a valid part of the value
+ int firstEq = stringToParse.indexOf('=');
+ if (firstEq == -1) {
+ key = urlDecode(stringToParse);
+ value = null;
+ } else {
+ key = urlDecode(stringToParse.substring(0, firstEq));
+ value = urlDecode(stringToParse.substring(firstEq + 1));
+ }
+ Collection values = map.containsKey(key) ? map.get(key) : new ArrayList<>();
+ values.add(value);
+ map.put(key, values);
+ }
+
+ public HttpRequest resolve(Map paramMap) throws UnsupportedEncodingException {
+ replaceQueryValues(paramMap);
+ Map encoded = new LinkedHashMap<>();
+ for (Map.Entry entry : paramMap.entrySet()) {
+ encoded.put(entry.getKey(), urlEncode(String.valueOf(entry.getValue())));
+ }
+ String resolvedUrl = expand(url.toString(), encoded).replace("+", "%20");
+ if (decodeSlash) {
+ resolvedUrl = resolvedUrl.replace("%2F", "/");
+ }
+ url = new StringBuilder(resolvedUrl);
+
+ Map> resolvedHeaders = new LinkedHashMap<>();
+ for (String field : headers.keySet()) {
+ Collection resolvedValues = new ArrayList<>();
+ for (String value : valuesOrEmpty(headers, field)) {
+ String resolved = expand(value, paramMap);
+ resolvedValues.add(resolved);
+ }
+ resolvedHeaders.put(field, resolvedValues);
+ }
+ headers.clear();
+ headers.putAll(resolvedHeaders);
+ if (bodyTemplate != null) {
+ body(urlDecode(expand(bodyTemplate, encoded)));
+ }
+ return this;
+ }
+
+ public HttpRequest method(String method) {
+ Asserts.notNull(method, "method can't be null");
+ Asserts.isTrue(method.matches("^[A-Z]+$"), "Invalid HTTP Method: " + method);
+ this.method = method;
+ return this;
+ }
+
+ /* @see Request#method() */
+ public String method() {
+ return method;
+ }
+
+ public HttpRequest decodeSlash(boolean decodeSlash) {
+ this.decodeSlash = decodeSlash;
+ return this;
+ }
+
+ public boolean decodeSlash() {
+ return decodeSlash;
+ }
+
+ /* @see #url() */
+ public HttpRequest append(CharSequence value) throws UnsupportedEncodingException {
+ url.append(value);
+ url = pullAnyQueriesOutOfUrl(url);
+ return this;
+ }
+
+ /* @see #url() */
+ public HttpRequest insert(int pos, CharSequence value) throws UnsupportedEncodingException {
+ if (isHttpUrl(value)) {
+ value = removeTrailingSlash(value);
+ if (url.length() > 0 && url.charAt(0) != '/') {
+ url.insert(0, '/');
+ }
+ }
+ url.insert(pos, pullAnyQueriesOutOfUrl(new StringBuilder(value)));
+ return this;
+ }
+
+ public String url() {
+ return url.toString();
+ }
+
+ /**
+ * Replaces queries with the specified {@code name} with the {@code values} supplied.
+ * Values can be passed in decoded or in url-encoded form depending on the value of the
+ * {@code encoded} parameter.
+ * When the {@code value} is {@code null}, all queries with the {@code configKey} are
+ * removed.
relationship to JAXRS 2.0
Like {@code WebTarget.query},
+ * except the values can be templatized. ex.
+ *
+ * Note: behavior of RequestTemplate is not consistent if a query parameter with
+ * unsafe characters is passed as both encoded and unencoded, although no validation is performed.
+ * ex.
+ *