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