From a56077496649c9aa4838e085d729f536d15a0bda Mon Sep 17 00:00:00 2001
From: fangjian0423 <fangjian0423@gmail.com>
Date: Thu, 6 Dec 2018 11:50:48 +0800
Subject: [PATCH] Sentinel support feign and close #6

---
 spring-cloud-alibaba-sentinel/pom.xml         |   7 +
 .../feign/SentinelContractHolder.java         |  55 ++++++
 .../alibaba/sentinel/feign/SentinelFeign.java | 148 +++++++++++++++
 .../feign/SentinelFeignAutoConfiguration.java |  45 +++++
 .../feign/SentinelInvocationHandler.java      | 172 ++++++++++++++++++
 .../main/resources/META-INF/spring.factories  |   3 +-
 6 files changed, 429 insertions(+), 1 deletion(-)
 create mode 100644 spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelContractHolder.java
 create mode 100644 spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeign.java
 create mode 100644 spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeignAutoConfiguration.java
 create mode 100644 spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelInvocationHandler.java

diff --git a/spring-cloud-alibaba-sentinel/pom.xml b/spring-cloud-alibaba-sentinel/pom.xml
index b4f63c9f2..d635b2a0b 100644
--- a/spring-cloud-alibaba-sentinel/pom.xml
+++ b/spring-cloud-alibaba-sentinel/pom.xml
@@ -35,6 +35,13 @@
             <artifactId>sentinel-dubbo-adapter</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId>
diff --git a/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelContractHolder.java b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelContractHolder.java
new file mode 100644
index 000000000..6056e800f
--- /dev/null
+++ b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelContractHolder.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.alibaba.sentinel.feign;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import feign.Contract;
+import feign.MethodMetadata;
+
+/**
+ *
+ * Using static field {@link SentinelContractHolder#metadataMap} to hold
+ * {@link MethodMetadata} data
+ *
+ * @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
+ */
+public class SentinelContractHolder implements Contract {
+
+	private final Contract delegate;
+
+	/**
+	 * map key is constructed by ClassFullName + configKey. configKey is constructed by
+	 * {@link feign.Feign#configKey}
+	 */
+	public final static Map<String, MethodMetadata> metadataMap = new HashMap();
+
+	public SentinelContractHolder(Contract delegate) {
+		this.delegate = delegate;
+	}
+
+	@Override
+	public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
+		List<MethodMetadata> metadatas = delegate.parseAndValidatateMetadata(targetType);
+		metadatas.forEach(metadata -> metadataMap
+				.put(targetType.getName() + metadata.configKey(), metadata));
+		return metadatas;
+	}
+
+}
diff --git a/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeign.java b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeign.java
new file mode 100644
index 000000000..5da3c1bcb
--- /dev/null
+++ b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeign.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.alibaba.sentinel.feign;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import org.springframework.beans.BeansException;
+import org.springframework.cloud.openfeign.FeignContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.util.ReflectionUtils;
+
+import feign.Contract;
+import feign.Feign;
+import feign.InvocationHandlerFactory;
+import feign.Target;
+import feign.hystrix.FallbackFactory;
+import feign.hystrix.HystrixFeign;
+
+/**
+ * {@link Feign.Builder} like {@link HystrixFeign.Builder}
+ *
+ * @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
+ */
+public class SentinelFeign {
+
+	public static Builder builder() {
+		return new Builder();
+	}
+
+	public static final class Builder extends Feign.Builder
+			implements ApplicationContextAware {
+
+		private Contract contract = new Contract.Default();
+
+		private ApplicationContext applicationContext;
+
+		private FeignContext feignContext;
+
+		@Override
+		public Feign.Builder invocationHandlerFactory(
+				InvocationHandlerFactory invocationHandlerFactory) {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override
+		public Builder contract(Contract contract) {
+			this.contract = contract;
+			return this;
+		}
+
+		@Override
+		public Feign build() {
+			super.invocationHandlerFactory(new InvocationHandlerFactory() {
+				@Override
+				public InvocationHandler create(Target target,
+						Map<Method, MethodHandler> dispatch) {
+					// using reflect get fallback and fallbackFactory properties from
+					// FeignClientFactoryBean because FeignClientFactoryBean is a package
+					// level class, we can not use it in our package
+					Object feignClientFactoryBean = Builder.this.applicationContext
+							.getBean("&" + target.type().getName());
+
+					Class fallback = (Class) getFieldValue(feignClientFactoryBean,
+							"fallback");
+					Class fallbackFactory = (Class) getFieldValue(feignClientFactoryBean,
+							"fallbackFactory");
+					String name = (String) getFieldValue(feignClientFactoryBean, "name");
+
+					Object fallbackInstance;
+					FallbackFactory fallbackFactoryInstance;
+					// check fallback and fallbackFactory properties
+					if (void.class != fallback) {
+						fallbackInstance = getFromContext(name, "fallback", fallback,
+								target);
+						return new SentinelInvocationHandler(target, dispatch,
+								new FallbackFactory.Default(fallbackInstance));
+					}
+					if (void.class != fallbackFactory) {
+						fallbackFactoryInstance = (FallbackFactory) getFromContext(name,
+								"fallbackFactory", fallbackFactory, target);
+						return new SentinelInvocationHandler(target, dispatch,
+								fallbackFactoryInstance);
+					}
+					return new SentinelInvocationHandler(target, dispatch);
+				}
+
+				private Object getFromContext(String name, String type,
+						Class fallbackType, Target target) {
+					Object fallbackInstance = feignContext.getInstance(name,
+							fallbackType);
+					if (fallbackInstance == null) {
+						throw new IllegalStateException(String.format(
+								"No %s instance of type %s found for feign client %s",
+								type, fallbackType, name));
+					}
+
+					if (!target.type().isAssignableFrom(fallbackType)) {
+						throw new IllegalStateException(String.format(
+								"Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
+								type, fallbackType, target.type(), name));
+					}
+					return fallbackInstance;
+				}
+			});
+
+			super.contract(new SentinelContractHolder(contract));
+			return super.build();
+		}
+
+		private Object getFieldValue(Object instance, String fieldName) {
+			Field field = ReflectionUtils.findField(instance.getClass(), fieldName);
+			field.setAccessible(true);
+			try {
+				return field.get(instance);
+			}
+			catch (IllegalAccessException e) {
+				// ignore
+			}
+			return null;
+		}
+
+		@Override
+		public void setApplicationContext(ApplicationContext applicationContext)
+				throws BeansException {
+			this.applicationContext = applicationContext;
+			feignContext = this.applicationContext.getBean(FeignContext.class);
+		}
+	}
+
+}
diff --git a/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeignAutoConfiguration.java b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeignAutoConfiguration.java
new file mode 100644
index 000000000..1ed20e1b3
--- /dev/null
+++ b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelFeignAutoConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.alibaba.sentinel.feign;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+
+import com.alibaba.csp.sentinel.SphU;
+
+import feign.Feign;
+
+/**
+ * @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
+ */
+@Configuration
+@ConditionalOnClass({ SphU.class, Feign.class })
+public class SentinelFeignAutoConfiguration {
+
+	@Bean
+	@Scope("prototype")
+	@ConditionalOnMissingBean
+	@ConditionalOnProperty(name = "feign.sentinel.enabled")
+	public Feign.Builder feignSentinelBuilder() {
+		return SentinelFeign.builder();
+	}
+
+}
diff --git a/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelInvocationHandler.java b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelInvocationHandler.java
new file mode 100644
index 000000000..809879cc7
--- /dev/null
+++ b/spring-cloud-alibaba-sentinel/src/main/java/org/springframework/cloud/alibaba/sentinel/feign/SentinelInvocationHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.alibaba.sentinel.feign;
+
+import static feign.Util.checkNotNull;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.alibaba.csp.sentinel.Entry;
+import com.alibaba.csp.sentinel.EntryType;
+import com.alibaba.csp.sentinel.SphU;
+import com.alibaba.csp.sentinel.Tracer;
+import com.alibaba.csp.sentinel.context.ContextUtil;
+import com.alibaba.csp.sentinel.slots.block.BlockException;
+
+import feign.Feign;
+import feign.InvocationHandlerFactory.MethodHandler;
+import feign.MethodMetadata;
+import feign.Target;
+import feign.hystrix.FallbackFactory;
+
+/**
+ * {@link InvocationHandler} handle invocation that protected by Sentinel
+ *
+ * @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
+ */
+public class SentinelInvocationHandler implements InvocationHandler {
+
+	private final Target<?> target;
+	private final Map<Method, MethodHandler> dispatch;
+
+	private FallbackFactory fallbackFactory;
+	private Map<Method, Method> fallbackMethodMap;
+
+	SentinelInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch,
+			FallbackFactory fallbackFactory) {
+		this.target = checkNotNull(target, "target");
+		this.dispatch = checkNotNull(dispatch, "dispatch");
+		this.fallbackFactory = fallbackFactory;
+		this.fallbackMethodMap = toFallbackMethod(dispatch);
+	}
+
+	SentinelInvocationHandler(Target<?> target, Map<Method, MethodHandler> dispatch) {
+		this.target = checkNotNull(target, "target");
+		this.dispatch = checkNotNull(dispatch, "dispatch");
+	}
+
+	@Override
+	public Object invoke(final Object proxy, final Method method, final Object[] args)
+			throws Throwable {
+		if ("equals".equals(method.getName())) {
+			try {
+				Object otherHandler = args.length > 0 && args[0] != null
+						? Proxy.getInvocationHandler(args[0])
+						: null;
+				return equals(otherHandler);
+			}
+			catch (IllegalArgumentException e) {
+				return false;
+			}
+		}
+		else if ("hashCode".equals(method.getName())) {
+			return hashCode();
+		}
+		else if ("toString".equals(method.getName())) {
+			return toString();
+		}
+
+		Object result;
+		MethodHandler methodHandler = this.dispatch.get(method);
+		// only handle by HardCodedTarget
+		if (target instanceof Target.HardCodedTarget) {
+			Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
+			MethodMetadata methodMetadata = SentinelContractHolder.metadataMap
+					.get(method.getDeclaringClass().getName()
+							+ Feign.configKey(method.getDeclaringClass(), method));
+			// resource default is HttpMethod:protocol://url
+			String resourceName = methodMetadata.template().method().toUpperCase() + ":"
+					+ hardCodedTarget.url() + methodMetadata.template().url();
+			Entry entry = null;
+			try {
+				ContextUtil.enter(resourceName);
+				entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
+				result = methodHandler.invoke(args);
+			}
+			catch (Throwable ex) {
+				// fallback handle
+				if (!BlockException.isBlockException(ex)) {
+					Tracer.trace(ex);
+				}
+				if (fallbackFactory != null) {
+					try {
+						Object fallbackResult = fallbackMethodMap.get(method)
+								.invoke(fallbackFactory.create(ex), args);
+						return fallbackResult;
+					}
+					catch (IllegalAccessException e) {
+						// shouldn't happen as method is public due to being an interface
+						throw new AssertionError(e);
+					}
+					catch (InvocationTargetException e) {
+						throw new AssertionError(e.getCause());
+					}
+				}
+				else {
+					// throw exception if fallbackFactory is null
+					throw ex;
+				}
+			}
+			finally {
+				if (entry != null) {
+					entry.exit();
+				}
+				ContextUtil.exit();
+			}
+		}
+		else {
+			// other target type using default strategy
+			result = methodHandler.invoke(args);
+		}
+
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj instanceof SentinelInvocationHandler) {
+			SentinelInvocationHandler other = (SentinelInvocationHandler) obj;
+			return target.equals(other.target);
+		}
+		return false;
+	}
+
+	@Override
+	public int hashCode() {
+		return target.hashCode();
+	}
+
+	@Override
+	public String toString() {
+		return target.toString();
+	}
+
+	static Map<Method, Method> toFallbackMethod(Map<Method, MethodHandler> dispatch) {
+		Map<Method, Method> result = new LinkedHashMap<>();
+		for (Method method : dispatch.keySet()) {
+			method.setAccessible(true);
+			result.put(method, method);
+		}
+		return result;
+	}
+
+}
diff --git a/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/spring.factories b/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/spring.factories
index ae146be44..702bb5bbd 100644
--- a/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-alibaba-sentinel/src/main/resources/META-INF/spring.factories
@@ -1,4 +1,5 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 org.springframework.cloud.alibaba.sentinel.SentinelWebAutoConfiguration,\
 org.springframework.cloud.alibaba.sentinel.endpoint.SentinelEndpointAutoConfiguration,\
-org.springframework.cloud.alibaba.sentinel.custom.SentinelAutoConfiguration
+org.springframework.cloud.alibaba.sentinel.custom.SentinelAutoConfiguration,\
+org.springframework.cloud.alibaba.sentinel.feign.SentinelFeignAutoConfiguration